swarm_sdk 2.7.14 → 3.0.0.alpha2
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 +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- 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/document_converters/base.rb +84 -0
- data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
- data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
- data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -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 +213 -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 +88 -149
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- 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 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- 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 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- 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 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- 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 -248
- 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 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- 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 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- 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 -973
- 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 -721
|
@@ -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,213 @@
|
|
|
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
|
+
# Supports document formats (PDF, DOCX, XLSX) if gems installed.
|
|
11
|
+
class Read < Base
|
|
12
|
+
# Document converters (optional gems)
|
|
13
|
+
CONVERTERS = [
|
|
14
|
+
DocumentConverters::PdfConverter,
|
|
15
|
+
DocumentConverters::DocxConverter,
|
|
16
|
+
DocumentConverters::XlsxConverter,
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
21
|
+
def creation_requirements
|
|
22
|
+
[:agent_name, :directory, :read_tracker]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
description <<~DESC
|
|
27
|
+
Reads a file from the local filesystem.
|
|
28
|
+
|
|
29
|
+
Supports text files with line numbers. Binary files (images) are returned as visual content.
|
|
30
|
+
Supports document formats (PDF, DOCX, XLSX) if gems installed.
|
|
31
|
+
|
|
32
|
+
Path handling:
|
|
33
|
+
- Relative paths resolve against your working directory
|
|
34
|
+
- Absolute paths (starting with /) are used as-is
|
|
35
|
+
DESC
|
|
36
|
+
|
|
37
|
+
param :file_path,
|
|
38
|
+
type: "string",
|
|
39
|
+
desc: "Path to the file to read",
|
|
40
|
+
required: true
|
|
41
|
+
|
|
42
|
+
param :offset,
|
|
43
|
+
type: "integer",
|
|
44
|
+
desc: "Line number to start reading from (1-indexed). Use for large text files. Ignored for documents.",
|
|
45
|
+
required: false
|
|
46
|
+
|
|
47
|
+
param :limit,
|
|
48
|
+
type: "integer",
|
|
49
|
+
desc: "Number of lines to read. Use for large text files. Ignored for documents.",
|
|
50
|
+
required: false
|
|
51
|
+
|
|
52
|
+
# @param agent_name [Symbol, String] Agent identifier for read tracking
|
|
53
|
+
# @param directory [String] Agent's working directory
|
|
54
|
+
# @param read_tracker [ReadTracker] Shared read tracker for cross-tool enforcement
|
|
55
|
+
def initialize(agent_name:, directory:, read_tracker:)
|
|
56
|
+
super()
|
|
57
|
+
@agent_name = agent_name.to_sym
|
|
58
|
+
@directory = File.expand_path(directory)
|
|
59
|
+
@read_tracker = read_tracker
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if a file has been read by this agent
|
|
63
|
+
#
|
|
64
|
+
# @param path [String] Resolved file path
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def file_read?(path)
|
|
67
|
+
@read_tracker.file_read?(@agent_name, path)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Execute file read
|
|
71
|
+
#
|
|
72
|
+
# @param file_path [String] Path to the file
|
|
73
|
+
# @param offset [Integer, nil] Starting line number (1-indexed)
|
|
74
|
+
# @param limit [Integer, nil] Number of lines to read
|
|
75
|
+
# @return [String, RubyLLM::Content] File contents or error message
|
|
76
|
+
def execute(file_path:, offset: nil, limit: nil)
|
|
77
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
78
|
+
|
|
79
|
+
resolved_path = resolve_path(file_path)
|
|
80
|
+
|
|
81
|
+
return validation_error("File does not exist: #{file_path}") unless File.exist?(resolved_path)
|
|
82
|
+
return validation_error("Path is a directory. Use Bash with ls to list directories.") if File.directory?(resolved_path)
|
|
83
|
+
|
|
84
|
+
# Try document converter first
|
|
85
|
+
converter_class = find_converter(resolved_path)
|
|
86
|
+
if converter_class
|
|
87
|
+
result = converter_class.new.convert(resolved_path)
|
|
88
|
+
|
|
89
|
+
# Register read for successful conversions
|
|
90
|
+
unless result.start_with?("<system-reminder>") || result.start_with?("Error:")
|
|
91
|
+
@read_tracker.register_read(@agent_name, resolved_path)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Standard text file handling
|
|
98
|
+
content = read_file_content(resolved_path)
|
|
99
|
+
|
|
100
|
+
# Binary file — return as-is
|
|
101
|
+
return content if content.is_a?(RubyLLM::Content)
|
|
102
|
+
return content if content.start_with?("Error:")
|
|
103
|
+
|
|
104
|
+
@read_tracker.register_read(@agent_name, resolved_path)
|
|
105
|
+
|
|
106
|
+
return format_empty_file if content.empty?
|
|
107
|
+
|
|
108
|
+
format_text_content(content, file_path, offset, limit)
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
error("Unexpected error reading file: #{e.class.name} - #{e.message}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# @return [String] Config accessor
|
|
116
|
+
def config
|
|
117
|
+
Configuration.instance
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Read file content, handling encoding
|
|
121
|
+
#
|
|
122
|
+
# @param file_path [String] Resolved absolute path
|
|
123
|
+
# @return [String, RubyLLM::Content] Text content or binary content object
|
|
124
|
+
def read_file_content(file_path)
|
|
125
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
126
|
+
|
|
127
|
+
unless content.valid_encoding?
|
|
128
|
+
return binary_or_unsupported(file_path)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
content
|
|
132
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
133
|
+
binary_or_unsupported(file_path)
|
|
134
|
+
rescue Errno::EACCES
|
|
135
|
+
error("Permission denied: Cannot read file '#{file_path}'")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Handle binary files
|
|
139
|
+
#
|
|
140
|
+
# @param file_path [String] Path to binary file
|
|
141
|
+
# @return [RubyLLM::Content, String] Content object for images, error for others
|
|
142
|
+
def binary_or_unsupported(file_path)
|
|
143
|
+
ext = File.extname(file_path).downcase
|
|
144
|
+
image_formats = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", ".svg", ".ico"]
|
|
145
|
+
|
|
146
|
+
if image_formats.include?(ext)
|
|
147
|
+
RubyLLM::Content.new("File: #{File.basename(file_path)}", file_path)
|
|
148
|
+
else
|
|
149
|
+
"Error: File contains binary data and cannot be displayed as text."
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Format empty file output
|
|
154
|
+
#
|
|
155
|
+
# @return [String]
|
|
156
|
+
def format_empty_file
|
|
157
|
+
"<system-reminder>Warning: This file exists but has empty contents.</system-reminder>"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Format text content with line numbers
|
|
161
|
+
#
|
|
162
|
+
# @param content [String] Raw file content
|
|
163
|
+
# @param file_path [String] Original file path for display
|
|
164
|
+
# @param offset [Integer, nil] Starting line (1-indexed)
|
|
165
|
+
# @param limit [Integer, nil] Lines to read
|
|
166
|
+
# @return [String] Formatted output with line numbers
|
|
167
|
+
def format_text_content(content, file_path, offset, limit)
|
|
168
|
+
lines = content.lines
|
|
169
|
+
total_lines = lines.count
|
|
170
|
+
|
|
171
|
+
start_line = offset ? [offset - 1, 0].max : 0
|
|
172
|
+
return validation_error("Offset #{offset} exceeds file length (#{total_lines} lines)") if start_line >= total_lines
|
|
173
|
+
|
|
174
|
+
lines = lines.drop(start_line)
|
|
175
|
+
|
|
176
|
+
default_limit = config.read_line_limit
|
|
177
|
+
effective_limit = limit || default_limit
|
|
178
|
+
lines = lines.take(effective_limit)
|
|
179
|
+
truncated = limit.nil? && total_lines > default_limit
|
|
180
|
+
|
|
181
|
+
max_line_length = config.line_character_limit
|
|
182
|
+
output_lines = lines.each_with_index.map do |line, idx|
|
|
183
|
+
line_number = start_line + idx + 1
|
|
184
|
+
display_line = line.chomp
|
|
185
|
+
display_line = "#{display_line[0...max_line_length]}... (line truncated)" if display_line.length > max_line_length
|
|
186
|
+
"#{line_number.to_s.rjust(6)}\t#{display_line}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
output = output_lines.join("\n")
|
|
190
|
+
output += truncation_notice(total_lines, default_limit) if truncated
|
|
191
|
+
output
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @param total_lines [Integer] Total lines in file
|
|
195
|
+
# @param limit [Integer] Applied limit
|
|
196
|
+
# @return [String] Truncation notice
|
|
197
|
+
def truncation_notice(total_lines, limit)
|
|
198
|
+
"\n\n<system-reminder>This file has #{total_lines} lines but only the first #{limit} are shown. " \
|
|
199
|
+
"Use offset and limit parameters to read more.</system-reminder>"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Find appropriate document converter for file extension
|
|
203
|
+
#
|
|
204
|
+
# @param file_path [String] Resolved file path
|
|
205
|
+
# @return [Class, nil] Converter class or nil if no match
|
|
206
|
+
def find_converter(file_path)
|
|
207
|
+
ext = File.extname(file_path).downcase
|
|
208
|
+
CONVERTERS.find { |c| c.extensions.include?(ext) }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
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
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Registry for V3 tools (built-in and custom)
|
|
7
|
+
#
|
|
8
|
+
# Maps tool names (symbols) to their V3 tool classes.
|
|
9
|
+
# Provides lookup, validation, factory, and custom tool registration.
|
|
10
|
+
#
|
|
11
|
+
# Tools fall into categories based on creation requirements:
|
|
12
|
+
# 1. **No params**: Simple tools (Think, Clock)
|
|
13
|
+
# 2. **Directory only**: Tools needing working directory (Bash, Grep, Glob)
|
|
14
|
+
# 3. **Agent context**: Tools needing agent name + directory (Read, Write, Edit)
|
|
15
|
+
#
|
|
16
|
+
# @example Look up a tool
|
|
17
|
+
# klass = Registry.get(:Read)
|
|
18
|
+
#
|
|
19
|
+
# @example Create a tool instance
|
|
20
|
+
# tool = Registry.create(:Read, agent_name: :backend, directory: "/app")
|
|
21
|
+
#
|
|
22
|
+
# @example Register a custom tool
|
|
23
|
+
# Registry.register(:MyTool, MyCustomTool)
|
|
24
|
+
#
|
|
25
|
+
# @example List available tools
|
|
26
|
+
# Registry.available_names #=> [:Read, :Write, :Edit, ...]
|
|
27
|
+
class Registry
|
|
28
|
+
class << self
|
|
29
|
+
# Lazily-built tool mapping
|
|
30
|
+
#
|
|
31
|
+
# Uses lazy evaluation so tool classes are only resolved when first accessed,
|
|
32
|
+
# allowing Zeitwerk to load them on demand. Mutable to support custom registration.
|
|
33
|
+
#
|
|
34
|
+
# @return [Hash<Symbol, Class>] Tool name to class mapping
|
|
35
|
+
def builtin_tools
|
|
36
|
+
@builtin_tools ||= {
|
|
37
|
+
Read: SwarmSDK::V3::Tools::Read,
|
|
38
|
+
Write: SwarmSDK::V3::Tools::Write,
|
|
39
|
+
Edit: SwarmSDK::V3::Tools::Edit,
|
|
40
|
+
Bash: SwarmSDK::V3::Tools::Bash,
|
|
41
|
+
Grep: SwarmSDK::V3::Tools::Grep,
|
|
42
|
+
Glob: SwarmSDK::V3::Tools::Glob,
|
|
43
|
+
Think: SwarmSDK::V3::Tools::Think,
|
|
44
|
+
Clock: SwarmSDK::V3::Tools::Clock,
|
|
45
|
+
SubTask: SwarmSDK::V3::Tools::SubTask,
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Register a custom tool
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol, String] Tool name
|
|
52
|
+
# @param klass [Class] Tool class (must inherit from Base or RubyLLM::Tool)
|
|
53
|
+
# @return [void]
|
|
54
|
+
# @raise [ConfigurationError] If name is already taken by a built-in tool
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# Registry.register(:WebSearch, MyWebSearchTool)
|
|
58
|
+
def register(name, klass)
|
|
59
|
+
name_sym = name.to_sym
|
|
60
|
+
builtin_tools[name_sym] = klass
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Reset registry to built-in tools only
|
|
64
|
+
#
|
|
65
|
+
# Removes all custom tool registrations. Useful for test cleanup.
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
def reset!
|
|
69
|
+
@builtin_tools = nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get tool class by name
|
|
73
|
+
#
|
|
74
|
+
# Respects the `registered_tools` configuration filter. If `registered_tools`
|
|
75
|
+
# is set, only those tools are visible.
|
|
76
|
+
#
|
|
77
|
+
# @param name [Symbol, String] Tool name
|
|
78
|
+
# @return [Class, nil] Tool class or nil if not found/filtered
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# Registry.get(:Read) #=> SwarmSDK::V3::Tools::Read
|
|
82
|
+
def get(name)
|
|
83
|
+
name_sym = name.to_sym
|
|
84
|
+
allowed = Configuration.instance.registered_tools
|
|
85
|
+
return if allowed && !allowed.map(&:to_sym).include?(name_sym)
|
|
86
|
+
|
|
87
|
+
builtin_tools[name_sym]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Create a tool instance with context
|
|
91
|
+
#
|
|
92
|
+
# Uses the tool's `creation_requirements` to determine constructor params.
|
|
93
|
+
#
|
|
94
|
+
# @param name [Symbol, String] Tool name
|
|
95
|
+
# @param context [Hash] Available context for tool creation
|
|
96
|
+
# @option context [Symbol] :agent_name Agent identifier
|
|
97
|
+
# @option context [String] :directory Agent's working directory
|
|
98
|
+
# @return [RubyLLM::Tool] Instantiated tool
|
|
99
|
+
# @raise [ConfigurationError] If tool unknown or requirements unmet
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# tool = Registry.create(:Read, agent_name: :backend, directory: "/app")
|
|
103
|
+
def create(name, **context)
|
|
104
|
+
name_sym = name.to_sym
|
|
105
|
+
tool_class = get(name_sym)
|
|
106
|
+
|
|
107
|
+
raise ConfigurationError, "Unknown tool: #{name}" unless tool_class
|
|
108
|
+
|
|
109
|
+
if tool_class.respond_to?(:creation_requirements) && tool_class.creation_requirements.any?
|
|
110
|
+
requirements = tool_class.creation_requirements
|
|
111
|
+
params = extract_params(requirements, context, name)
|
|
112
|
+
tool_class.new(**params)
|
|
113
|
+
else
|
|
114
|
+
tool_class.new
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Create all tools for an agent definition
|
|
119
|
+
#
|
|
120
|
+
# Merges the agent's tools with global `default_tools` from configuration.
|
|
121
|
+
# Filters against `registered_tools` if configured.
|
|
122
|
+
#
|
|
123
|
+
# @param definition [AgentDefinition] Agent definition with tool list
|
|
124
|
+
# @return [Array<RubyLLM::Tool>] Instantiated tools
|
|
125
|
+
# @raise [ConfigurationError] If any tool is unknown
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# tools = Registry.create_all(definition)
|
|
129
|
+
def create_all(definition, memory_store: nil, subtask_depth: 0)
|
|
130
|
+
# Create shared read tracker for cross-tool enforcement
|
|
131
|
+
read_tracker = ReadTracker.new
|
|
132
|
+
|
|
133
|
+
context = {
|
|
134
|
+
agent_name: definition.name,
|
|
135
|
+
directory: definition.directory,
|
|
136
|
+
read_tracker: read_tracker,
|
|
137
|
+
memory_store: memory_store,
|
|
138
|
+
agent_definition: definition,
|
|
139
|
+
subtask_depth: subtask_depth,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
config = Configuration.instance
|
|
143
|
+
|
|
144
|
+
# Start with the agent's declared tools + global default_tools
|
|
145
|
+
tool_names = definition.tools.dup
|
|
146
|
+
config.default_tools.each do |name|
|
|
147
|
+
tool_names << name.to_sym unless tool_names.include?(name.to_sym)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Filter against registered_tools if configured
|
|
151
|
+
if config.registered_tools
|
|
152
|
+
allowed = config.registered_tools.map(&:to_sym)
|
|
153
|
+
tool_names.select! { |name| allowed.include?(name) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
tool_names.uniq.map { |name| create(name, **context) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if a tool exists (respects registered_tools filter)
|
|
160
|
+
#
|
|
161
|
+
# @param name [Symbol, String] Tool name
|
|
162
|
+
# @return [Boolean]
|
|
163
|
+
def exists?(name)
|
|
164
|
+
!get(name.to_sym).nil?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get all available tool names (respects registered_tools filter)
|
|
168
|
+
#
|
|
169
|
+
# @return [Array<Symbol>]
|
|
170
|
+
def available_names
|
|
171
|
+
allowed = Configuration.instance.registered_tools
|
|
172
|
+
names = builtin_tools.keys
|
|
173
|
+
names.select! { |n| allowed.map(&:to_sym).include?(n) } if allowed
|
|
174
|
+
names
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Validate tool names
|
|
178
|
+
#
|
|
179
|
+
# @param names [Array<Symbol, String>] Tool names to validate
|
|
180
|
+
# @return [Array<Symbol>] Invalid tool names
|
|
181
|
+
def validate(names)
|
|
182
|
+
names.map(&:to_sym).reject { |name| exists?(name) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Extract parameters from context for tool construction
|
|
188
|
+
#
|
|
189
|
+
# Includes each declared requirement that exists in the context.
|
|
190
|
+
# Missing keys are skipped — the tool constructor's own defaults
|
|
191
|
+
# and Ruby's `missing keyword` error handle validation naturally.
|
|
192
|
+
#
|
|
193
|
+
# @param requirements [Array<Symbol>] Parameter names the tool accepts
|
|
194
|
+
# @param context [Hash] Available context
|
|
195
|
+
# @param _tool_name [Symbol] Tool name (unused, kept for interface stability)
|
|
196
|
+
# @return [Hash] Parameters for constructor
|
|
197
|
+
def extract_params(requirements, context, _tool_name)
|
|
198
|
+
params = {}
|
|
199
|
+
requirements.each do |req|
|
|
200
|
+
params[req] = context[req] if context.key?(req)
|
|
201
|
+
end
|
|
202
|
+
params
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|