swarm_memory 2.1.2 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +30 -19
- data/lib/claude_swarm/mcp_generator.rb +5 -10
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +14 -14
- data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
- data/lib/swarm_memory/tools/memory_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +6 -1
- data/lib/swarm_sdk/agent/builder.rb +91 -0
- data/lib/swarm_sdk/agent/chat.rb +540 -925
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +8 -4
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +79 -174
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +100 -261
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +199 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +192 -16
- data/lib/swarm_sdk/log_stream.rb +66 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
- data/lib/swarm_sdk/state_restorer.rb +476 -0
- data/lib/swarm_sdk/state_snapshot.rb +334 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +69 -407
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
- data/lib/swarm_sdk/swarm.rb +366 -631
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +127 -24
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +28 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +27 -8
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/workflow.rb +554 -0
- data/lib/swarm_sdk.rb +393 -22
- metadata +51 -16
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/node_orchestrator.rb +0 -591
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Loader for creating swarm instances from multiple sources
|
|
5
|
+
#
|
|
6
|
+
# SwarmLoader loads swarm configurations from:
|
|
7
|
+
# - Files: .rb (DSL) or .yml (YAML)
|
|
8
|
+
# - YAML strings: Direct YAML content
|
|
9
|
+
# - DSL blocks: Inline Ruby blocks
|
|
10
|
+
#
|
|
11
|
+
# All loaded swarms get hierarchical swarm_id and parent_swarm_id.
|
|
12
|
+
#
|
|
13
|
+
# ## Features
|
|
14
|
+
# - Supports Ruby DSL (.rb files or blocks)
|
|
15
|
+
# - Supports YAML (.yml/.yaml files or strings)
|
|
16
|
+
# - Sets hierarchical swarm_id based on parent + registration name
|
|
17
|
+
# - Isolates loading in separate context
|
|
18
|
+
# - Proper error handling for missing/invalid sources
|
|
19
|
+
#
|
|
20
|
+
# ## Examples
|
|
21
|
+
#
|
|
22
|
+
# # From file
|
|
23
|
+
# swarm = SwarmLoader.load_from_file(
|
|
24
|
+
# "./swarms/code_review.rb",
|
|
25
|
+
# swarm_id: "main/code_review",
|
|
26
|
+
# parent_swarm_id: "main"
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
# # From YAML string
|
|
30
|
+
# swarm = SwarmLoader.load_from_yaml_string(
|
|
31
|
+
# "version: 2\nswarm:\n name: Test\n...",
|
|
32
|
+
# swarm_id: "main/testing",
|
|
33
|
+
# parent_swarm_id: "main"
|
|
34
|
+
# )
|
|
35
|
+
#
|
|
36
|
+
# # From block
|
|
37
|
+
# swarm = SwarmLoader.load_from_block(
|
|
38
|
+
# proc { id "team"; name "Team"; agent :dev { ... } },
|
|
39
|
+
# swarm_id: "main/team",
|
|
40
|
+
# parent_swarm_id: "main"
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
class SwarmLoader
|
|
44
|
+
class << self
|
|
45
|
+
# Load a swarm from a file (.rb or .yml)
|
|
46
|
+
#
|
|
47
|
+
# @param file_path [String] Path to swarm file
|
|
48
|
+
# @param swarm_id [String] Hierarchical swarm ID to assign
|
|
49
|
+
# @param parent_swarm_id [String] Parent swarm ID
|
|
50
|
+
# @return [Swarm] Loaded swarm instance with overridden IDs
|
|
51
|
+
# @raise [ConfigurationError] If file not found or unsupported type
|
|
52
|
+
def load_from_file(file_path, swarm_id:, parent_swarm_id:)
|
|
53
|
+
path = Pathname.new(file_path).expand_path
|
|
54
|
+
|
|
55
|
+
raise ConfigurationError, "Swarm file not found: #{path}" unless path.exist?
|
|
56
|
+
|
|
57
|
+
# Determine file type and load
|
|
58
|
+
case path.extname
|
|
59
|
+
when ".rb"
|
|
60
|
+
load_from_ruby_file(path, swarm_id, parent_swarm_id)
|
|
61
|
+
when ".yml", ".yaml"
|
|
62
|
+
load_from_yaml_file(path, swarm_id, parent_swarm_id)
|
|
63
|
+
else
|
|
64
|
+
raise ConfigurationError, "Unsupported swarm file type: #{path.extname}. Use .rb, .yml, or .yaml"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Load a swarm from YAML string
|
|
69
|
+
#
|
|
70
|
+
# @param yaml_content [String] YAML configuration content
|
|
71
|
+
# @param swarm_id [String] Hierarchical swarm ID to assign
|
|
72
|
+
# @param parent_swarm_id [String] Parent swarm ID
|
|
73
|
+
# @return [Swarm] Loaded swarm instance with overridden IDs
|
|
74
|
+
# @raise [ConfigurationError] If YAML is invalid
|
|
75
|
+
def load_from_yaml_string(yaml_content, swarm_id:, parent_swarm_id:)
|
|
76
|
+
# Use Configuration to parse YAML string
|
|
77
|
+
config = Configuration.new(yaml_content, base_dir: Dir.pwd)
|
|
78
|
+
config.load_and_validate
|
|
79
|
+
swarm = config.to_swarm
|
|
80
|
+
|
|
81
|
+
# Override swarm_id and parent_swarm_id
|
|
82
|
+
swarm.override_swarm_ids(swarm_id: swarm_id, parent_swarm_id: parent_swarm_id)
|
|
83
|
+
|
|
84
|
+
swarm
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Load a swarm from DSL block
|
|
88
|
+
#
|
|
89
|
+
# @param block [Proc] Block containing SwarmSDK DSL
|
|
90
|
+
# @param swarm_id [String] Hierarchical swarm ID to assign
|
|
91
|
+
# @param parent_swarm_id [String] Parent swarm ID
|
|
92
|
+
# @return [Swarm] Loaded swarm instance with overridden IDs
|
|
93
|
+
def load_from_block(block, swarm_id:, parent_swarm_id:)
|
|
94
|
+
# Execute block in Builder context
|
|
95
|
+
builder = Swarm::Builder.new
|
|
96
|
+
builder.instance_eval(&block)
|
|
97
|
+
swarm = builder.build_swarm
|
|
98
|
+
|
|
99
|
+
# Override swarm_id and parent_swarm_id
|
|
100
|
+
swarm.override_swarm_ids(swarm_id: swarm_id, parent_swarm_id: parent_swarm_id)
|
|
101
|
+
|
|
102
|
+
swarm
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Load swarm from Ruby DSL file
|
|
108
|
+
#
|
|
109
|
+
# @param path [Pathname] Path to .rb file
|
|
110
|
+
# @param swarm_id [String] Swarm ID to assign
|
|
111
|
+
# @param parent_swarm_id [String] Parent swarm ID
|
|
112
|
+
# @return [Swarm] Loaded swarm with overridden IDs
|
|
113
|
+
def load_from_ruby_file(path, swarm_id, parent_swarm_id)
|
|
114
|
+
content = File.read(path)
|
|
115
|
+
|
|
116
|
+
# Execute DSL in isolated context
|
|
117
|
+
# The DSL should return a swarm via SwarmSDK.build { ... }
|
|
118
|
+
swarm = eval(content, binding, path.to_s) # rubocop:disable Security/Eval
|
|
119
|
+
|
|
120
|
+
# Override swarm_id and parent_swarm_id
|
|
121
|
+
# These must be set after build to ensure hierarchical structure
|
|
122
|
+
swarm.override_swarm_ids(swarm_id: swarm_id, parent_swarm_id: parent_swarm_id)
|
|
123
|
+
|
|
124
|
+
swarm
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Load swarm from YAML file
|
|
128
|
+
#
|
|
129
|
+
# @param path [Pathname] Path to .yml file
|
|
130
|
+
# @param swarm_id [String] Swarm ID to assign
|
|
131
|
+
# @param parent_swarm_id [String] Parent swarm ID
|
|
132
|
+
# @return [Swarm] Loaded swarm with overridden IDs
|
|
133
|
+
def load_from_yaml_file(path, swarm_id, parent_swarm_id)
|
|
134
|
+
# Use Configuration to load and convert YAML to swarm
|
|
135
|
+
config = Configuration.load_file(path.to_s)
|
|
136
|
+
swarm = config.to_swarm
|
|
137
|
+
|
|
138
|
+
# Override swarm_id and parent_swarm_id
|
|
139
|
+
swarm.override_swarm_ids(swarm_id: swarm_id, parent_swarm_id: parent_swarm_id)
|
|
140
|
+
|
|
141
|
+
swarm
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Registry for managing sub-swarms in composable swarms
|
|
5
|
+
#
|
|
6
|
+
# SwarmRegistry handles lazy loading, caching, and lifecycle management
|
|
7
|
+
# of child swarms registered via the `swarms` DSL block.
|
|
8
|
+
#
|
|
9
|
+
# ## Features
|
|
10
|
+
# - Lazy loading: Sub-swarms are only loaded when first accessed
|
|
11
|
+
# - Caching: Loaded swarms are cached for reuse
|
|
12
|
+
# - Hierarchical IDs: Sub-swarms get IDs based on parent + registration name
|
|
13
|
+
# - Context control: keep_context determines if swarm state persists
|
|
14
|
+
# - Lifecycle management: Cleanup cascades through all sub-swarms
|
|
15
|
+
#
|
|
16
|
+
# ## Example
|
|
17
|
+
#
|
|
18
|
+
# registry = SwarmRegistry.new(parent_swarm_id: "main_app")
|
|
19
|
+
# registry.register("code_review", file: "./swarms/code_review.rb", keep_context: true)
|
|
20
|
+
#
|
|
21
|
+
# # Lazy load on first access
|
|
22
|
+
# swarm = registry.load_swarm("code_review")
|
|
23
|
+
# # => Swarm with swarm_id = "main_app/code_review"
|
|
24
|
+
#
|
|
25
|
+
# # Reset if keep_context: false
|
|
26
|
+
# registry.reset_if_needed("code_review")
|
|
27
|
+
#
|
|
28
|
+
class SwarmRegistry
|
|
29
|
+
# Initialize a new swarm registry
|
|
30
|
+
#
|
|
31
|
+
# @param parent_swarm_id [String] ID of the parent swarm
|
|
32
|
+
def initialize(parent_swarm_id:)
|
|
33
|
+
@parent_swarm_id = parent_swarm_id
|
|
34
|
+
@registered_swarms = {}
|
|
35
|
+
# Format: { "code_review" => { file: "...", keep_context: true, instance: nil } }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Register a sub-swarm for lazy loading
|
|
39
|
+
#
|
|
40
|
+
# @param name [String] Registration name for the swarm
|
|
41
|
+
# @param source [Hash] Source specification with :type and :value
|
|
42
|
+
# - { type: :file, value: "./path/to/swarm.rb" }
|
|
43
|
+
# - { type: :yaml, value: "version: 2\n..." }
|
|
44
|
+
# - { type: :block, value: Proc }
|
|
45
|
+
# @param keep_context [Boolean] Whether to preserve conversation state between calls (default: true)
|
|
46
|
+
# @return [void]
|
|
47
|
+
# @raise [ArgumentError] If swarm with same name already registered
|
|
48
|
+
def register(name, source:, keep_context: true)
|
|
49
|
+
raise ArgumentError, "Swarm '#{name}' already registered" if @registered_swarms.key?(name)
|
|
50
|
+
|
|
51
|
+
@registered_swarms[name] = {
|
|
52
|
+
source: source,
|
|
53
|
+
keep_context: keep_context,
|
|
54
|
+
instance: nil, # Lazy load
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if a swarm is registered
|
|
59
|
+
#
|
|
60
|
+
# @param name [String] Swarm registration name
|
|
61
|
+
# @return [Boolean] True if swarm is registered
|
|
62
|
+
def registered?(name)
|
|
63
|
+
@registered_swarms.key?(name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Load a registered swarm (lazy load + cache)
|
|
67
|
+
#
|
|
68
|
+
# Loads the swarm from its source (file, yaml, or block) on first access, then caches it.
|
|
69
|
+
# Sets hierarchical swarm_id based on parent_swarm_id + registration name.
|
|
70
|
+
#
|
|
71
|
+
# @param name [String] Swarm registration name
|
|
72
|
+
# @return [Swarm] Loaded swarm instance
|
|
73
|
+
# @raise [ConfigurationError] If swarm not registered
|
|
74
|
+
def load_swarm(name)
|
|
75
|
+
entry = @registered_swarms[name]
|
|
76
|
+
raise ConfigurationError, "Swarm '#{name}' not registered" unless entry
|
|
77
|
+
|
|
78
|
+
# Return cached instance if exists
|
|
79
|
+
return entry[:instance] if entry[:instance]
|
|
80
|
+
|
|
81
|
+
# Load from appropriate source
|
|
82
|
+
swarm_id = "#{@parent_swarm_id}/#{name}" # Hierarchical
|
|
83
|
+
source = entry[:source]
|
|
84
|
+
|
|
85
|
+
swarm = case source[:type]
|
|
86
|
+
when :file
|
|
87
|
+
SwarmLoader.load_from_file(
|
|
88
|
+
source[:value],
|
|
89
|
+
swarm_id: swarm_id,
|
|
90
|
+
parent_swarm_id: @parent_swarm_id,
|
|
91
|
+
)
|
|
92
|
+
when :yaml
|
|
93
|
+
SwarmLoader.load_from_yaml_string(
|
|
94
|
+
source[:value],
|
|
95
|
+
swarm_id: swarm_id,
|
|
96
|
+
parent_swarm_id: @parent_swarm_id,
|
|
97
|
+
)
|
|
98
|
+
when :block
|
|
99
|
+
SwarmLoader.load_from_block(
|
|
100
|
+
source[:value],
|
|
101
|
+
swarm_id: swarm_id,
|
|
102
|
+
parent_swarm_id: @parent_swarm_id,
|
|
103
|
+
)
|
|
104
|
+
else
|
|
105
|
+
raise ConfigurationError, "Unknown source type: #{source[:type]}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
entry[:instance] = swarm
|
|
109
|
+
swarm
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Reset swarm context if keep_context: false
|
|
113
|
+
#
|
|
114
|
+
# @param name [String] Swarm registration name
|
|
115
|
+
# @return [void]
|
|
116
|
+
def reset_if_needed(name)
|
|
117
|
+
entry = @registered_swarms[name]
|
|
118
|
+
return if entry[:keep_context]
|
|
119
|
+
|
|
120
|
+
entry[:instance]&.reset_context!
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Cleanup all registered swarms
|
|
124
|
+
#
|
|
125
|
+
# Stops all loaded swarm instances and clears the registry.
|
|
126
|
+
# Should be called when parent swarm is done.
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
def shutdown_all
|
|
130
|
+
@registered_swarms.each_value do |entry|
|
|
131
|
+
entry[:instance]&.cleanup
|
|
132
|
+
end
|
|
133
|
+
@registered_swarms.clear
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/swarm_sdk/tools/bash.rb
CHANGED
|
@@ -7,6 +7,13 @@ module SwarmSDK
|
|
|
7
7
|
# Executes commands in a persistent shell session with timeout support.
|
|
8
8
|
# Provides comprehensive guidance on proper usage patterns.
|
|
9
9
|
class Bash < RubyLLM::Tool
|
|
10
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
11
|
+
class << self
|
|
12
|
+
def creation_requirements
|
|
13
|
+
[:directory]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
def initialize(directory:)
|
|
11
18
|
super()
|
|
12
19
|
@directory = File.expand_path(directory)
|
|
@@ -78,9 +85,10 @@ module SwarmSDK
|
|
|
78
85
|
desc: "Optional timeout in milliseconds (max 600000)",
|
|
79
86
|
required: false
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
# Backward compatibility aliases - use Defaults module for new code
|
|
89
|
+
DEFAULT_TIMEOUT_MS = Defaults::Timeouts::BASH_COMMAND_MS
|
|
90
|
+
MAX_TIMEOUT_MS = Defaults::Timeouts::BASH_COMMAND_MAX_MS
|
|
91
|
+
MAX_OUTPUT_LENGTH = Defaults::Limits::OUTPUT_CHARACTERS
|
|
84
92
|
|
|
85
93
|
# Commands that are ALWAYS blocked for safety reasons
|
|
86
94
|
# These cannot be overridden by permissions configuration
|
|
@@ -2,22 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
4
|
module Tools
|
|
5
|
-
# Delegate tool for
|
|
5
|
+
# Delegate tool for working with other agents in the swarm
|
|
6
6
|
#
|
|
7
|
-
# Creates agent-specific
|
|
8
|
-
# that allow one agent to
|
|
7
|
+
# Creates agent-specific collaboration tools (e.g., WorkWithBackend)
|
|
8
|
+
# that allow one agent to work with another agent.
|
|
9
9
|
# Supports pre/post delegation hooks for customization.
|
|
10
10
|
class Delegate < RubyLLM::Tool
|
|
11
|
+
# Tool name prefix for delegation tools
|
|
12
|
+
# Change this to customize the tool naming pattern (e.g., "DelegateTaskTo", "AskAgent", etc.)
|
|
13
|
+
TOOL_NAME_PREFIX = "WorkWith"
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Generate tool name for a delegate agent
|
|
17
|
+
#
|
|
18
|
+
# This is the single source of truth for delegation tool naming.
|
|
19
|
+
# Used both when creating Delegate instances and when predicting tool names
|
|
20
|
+
# for agent context setup.
|
|
21
|
+
#
|
|
22
|
+
# @param delegate_name [String, Symbol] Name of the delegate agent
|
|
23
|
+
# @return [String] Tool name (e.g., "WorkWithBackend")
|
|
24
|
+
def tool_name_for(delegate_name)
|
|
25
|
+
"#{TOOL_NAME_PREFIX}#{delegate_name.to_s.capitalize}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
11
29
|
attr_reader :delegate_name, :delegate_target, :tool_name
|
|
12
30
|
|
|
13
31
|
# Initialize a delegation tool
|
|
14
32
|
#
|
|
15
33
|
# @param delegate_name [String] Name of the delegate agent (e.g., "backend")
|
|
16
34
|
# @param delegate_description [String] Description of the delegate agent
|
|
17
|
-
# @param delegate_chat [AgentChat] The chat instance for the delegate agent
|
|
35
|
+
# @param delegate_chat [AgentChat, nil] The chat instance for the delegate agent (nil if delegating to swarm)
|
|
18
36
|
# @param agent_name [Symbol, String] Name of the agent using this tool
|
|
19
|
-
# @param swarm [Swarm] The swarm instance
|
|
20
|
-
# @param hook_registry [Hooks::Registry] Registry for callbacks
|
|
37
|
+
# @param swarm [Swarm] The swarm instance (provides hook_registry, delegation_call_stack, swarm_registry)
|
|
21
38
|
# @param delegating_chat [Agent::Chat, nil] The chat instance of the agent doing the delegating (for accessing hooks)
|
|
22
39
|
def initialize(
|
|
23
40
|
delegate_name:,
|
|
@@ -25,7 +42,6 @@ module SwarmSDK
|
|
|
25
42
|
delegate_chat:,
|
|
26
43
|
agent_name:,
|
|
27
44
|
swarm:,
|
|
28
|
-
hook_registry:,
|
|
29
45
|
delegating_chat: nil
|
|
30
46
|
)
|
|
31
47
|
super()
|
|
@@ -35,22 +51,21 @@ module SwarmSDK
|
|
|
35
51
|
@delegate_chat = delegate_chat
|
|
36
52
|
@agent_name = agent_name
|
|
37
53
|
@swarm = swarm
|
|
38
|
-
@hook_registry = hook_registry
|
|
39
54
|
@delegating_chat = delegating_chat
|
|
40
55
|
|
|
41
|
-
# Generate tool name
|
|
42
|
-
@tool_name =
|
|
56
|
+
# Generate tool name using canonical method
|
|
57
|
+
@tool_name = self.class.tool_name_for(delegate_name)
|
|
43
58
|
@delegate_target = delegate_name.to_s
|
|
44
59
|
end
|
|
45
60
|
|
|
46
61
|
# Override description to return dynamic string based on delegate
|
|
47
62
|
def description
|
|
48
|
-
"
|
|
63
|
+
"Work with #{@delegate_name} to delegate work, ask questions, or collaborate. #{@delegate_description}"
|
|
49
64
|
end
|
|
50
65
|
|
|
51
|
-
param :
|
|
66
|
+
param :message,
|
|
52
67
|
type: "string",
|
|
53
|
-
desc: "
|
|
68
|
+
desc: "Message to send to the agent - can be a work request, question, or collaboration message",
|
|
54
69
|
required: true
|
|
55
70
|
|
|
56
71
|
# Override name to return custom delegation tool name
|
|
@@ -60,9 +75,21 @@ module SwarmSDK
|
|
|
60
75
|
|
|
61
76
|
# Execute delegation with pre/post hooks
|
|
62
77
|
#
|
|
63
|
-
# @param
|
|
78
|
+
# @param message [String] Message to send to the agent
|
|
64
79
|
# @return [String] Result from delegate agent or error message
|
|
65
|
-
def execute(
|
|
80
|
+
def execute(message:)
|
|
81
|
+
# Access swarm infrastructure
|
|
82
|
+
call_stack = @swarm.delegation_call_stack
|
|
83
|
+
hook_registry = @swarm.hook_registry
|
|
84
|
+
swarm_registry = @swarm.swarm_registry
|
|
85
|
+
|
|
86
|
+
# Check for circular dependency
|
|
87
|
+
if call_stack.include?(@delegate_target)
|
|
88
|
+
emit_circular_warning(call_stack)
|
|
89
|
+
return "Error: Circular delegation detected: #{call_stack.join(" -> ")} -> #{@delegate_target}. " \
|
|
90
|
+
"Please restructure your delegation to avoid infinite loops."
|
|
91
|
+
end
|
|
92
|
+
|
|
66
93
|
# Get agent-specific hooks from the delegating chat instance
|
|
67
94
|
agent_hooks = if @delegating_chat&.respond_to?(:hook_agent_hooks)
|
|
68
95
|
@delegating_chat.hook_agent_hooks || {}
|
|
@@ -78,12 +105,12 @@ module SwarmSDK
|
|
|
78
105
|
delegation_target: @delegate_target,
|
|
79
106
|
metadata: {
|
|
80
107
|
tool_name: @tool_name,
|
|
81
|
-
|
|
108
|
+
message: message,
|
|
82
109
|
timestamp: Time.now.utc.iso8601,
|
|
83
110
|
},
|
|
84
111
|
)
|
|
85
112
|
|
|
86
|
-
executor = Hooks::Executor.new(
|
|
113
|
+
executor = Hooks::Executor.new(hook_registry, logger: RubyLLM.logger)
|
|
87
114
|
pre_agent_hooks = agent_hooks[:pre_delegation] || []
|
|
88
115
|
result = executor.execute_safe(event: :pre_delegation, context: context, callbacks: pre_agent_hooks)
|
|
89
116
|
|
|
@@ -94,9 +121,16 @@ module SwarmSDK
|
|
|
94
121
|
return result.value
|
|
95
122
|
end
|
|
96
123
|
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
124
|
+
# Determine delegation type and proceed
|
|
125
|
+
delegation_result = if @delegate_chat
|
|
126
|
+
# Delegate to agent
|
|
127
|
+
delegate_to_agent(message, call_stack)
|
|
128
|
+
elsif swarm_registry&.registered?(@delegate_target)
|
|
129
|
+
# Delegate to registered swarm
|
|
130
|
+
delegate_to_swarm(message, call_stack, swarm_registry)
|
|
131
|
+
else
|
|
132
|
+
raise ConfigurationError, "Unknown delegation target: #{@delegate_target}"
|
|
133
|
+
end
|
|
100
134
|
|
|
101
135
|
# Trigger post_delegation callback
|
|
102
136
|
post_context = Hooks::Context.new(
|
|
@@ -107,7 +141,7 @@ module SwarmSDK
|
|
|
107
141
|
delegation_result: delegation_result,
|
|
108
142
|
metadata: {
|
|
109
143
|
tool_name: @tool_name,
|
|
110
|
-
|
|
144
|
+
message: message,
|
|
111
145
|
result: delegation_result,
|
|
112
146
|
timestamp: Time.now.utc.iso8601,
|
|
113
147
|
},
|
|
@@ -127,10 +161,12 @@ module SwarmSDK
|
|
|
127
161
|
LogStream.emit(
|
|
128
162
|
type: "delegation_error",
|
|
129
163
|
agent: @agent_name,
|
|
164
|
+
swarm_id: @swarm.swarm_id,
|
|
165
|
+
parent_swarm_id: @swarm.parent_swarm_id,
|
|
130
166
|
delegate_to: @tool_name,
|
|
131
167
|
error_class: e.class.name,
|
|
132
168
|
error_message: "Request timed out",
|
|
133
|
-
|
|
169
|
+
error_backtrace: e.backtrace&.first(5) || [],
|
|
134
170
|
)
|
|
135
171
|
"Error: Request to #{@tool_name} timed out. The agent may be overloaded or the LLM service is not responding. Please try again or simplify the task."
|
|
136
172
|
rescue Faraday::Error => e
|
|
@@ -138,10 +174,12 @@ module SwarmSDK
|
|
|
138
174
|
LogStream.emit(
|
|
139
175
|
type: "delegation_error",
|
|
140
176
|
agent: @agent_name,
|
|
177
|
+
swarm_id: @swarm.swarm_id,
|
|
178
|
+
parent_swarm_id: @swarm.parent_swarm_id,
|
|
141
179
|
delegate_to: @tool_name,
|
|
142
180
|
error_class: e.class.name,
|
|
143
181
|
error_message: e.message,
|
|
144
|
-
|
|
182
|
+
error_backtrace: e.backtrace&.first(5) || [],
|
|
145
183
|
)
|
|
146
184
|
"Error: Network error communicating with #{@tool_name}: #{e.class.name}. Please check connectivity and try again."
|
|
147
185
|
rescue StandardError => e
|
|
@@ -150,15 +188,80 @@ module SwarmSDK
|
|
|
150
188
|
LogStream.emit(
|
|
151
189
|
type: "delegation_error",
|
|
152
190
|
agent: @agent_name,
|
|
191
|
+
swarm_id: @swarm.swarm_id,
|
|
192
|
+
parent_swarm_id: @swarm.parent_swarm_id,
|
|
153
193
|
delegate_to: @tool_name,
|
|
154
194
|
error_class: e.class.name,
|
|
155
195
|
error_message: e.message,
|
|
156
|
-
|
|
196
|
+
error_backtrace: backtrace_array,
|
|
157
197
|
)
|
|
158
198
|
# Return error string for LLM
|
|
159
199
|
backtrace_str = backtrace_array.join("\n ")
|
|
160
200
|
"Error: #{@tool_name} encountered an error: #{e.class.name}: #{e.message}\nBacktrace:\n #{backtrace_str}"
|
|
161
201
|
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
# Delegate to an agent
|
|
206
|
+
#
|
|
207
|
+
# @param message [String] Message to send to the agent
|
|
208
|
+
# @param call_stack [Array] Delegation call stack for circular dependency detection
|
|
209
|
+
# @return [String] Result from agent
|
|
210
|
+
def delegate_to_agent(message, call_stack)
|
|
211
|
+
# Push delegate target onto call stack to track delegation chain
|
|
212
|
+
call_stack.push(@delegate_target)
|
|
213
|
+
begin
|
|
214
|
+
response = @delegate_chat.ask(message, source: "delegation")
|
|
215
|
+
response.content
|
|
216
|
+
ensure
|
|
217
|
+
# Always pop from stack, even if delegation fails
|
|
218
|
+
call_stack.pop
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Delegate to a registered swarm
|
|
223
|
+
#
|
|
224
|
+
# @param message [String] Message to send to the swarm
|
|
225
|
+
# @param call_stack [Array] Delegation call stack for circular dependency detection
|
|
226
|
+
# @param swarm_registry [SwarmRegistry] Registry for sub-swarms
|
|
227
|
+
# @return [String] Result from swarm's lead agent
|
|
228
|
+
def delegate_to_swarm(message, call_stack, swarm_registry)
|
|
229
|
+
# Load sub-swarm (lazy load + cache)
|
|
230
|
+
subswarm = swarm_registry.load_swarm(@delegate_target)
|
|
231
|
+
|
|
232
|
+
# Push delegate target onto call stack to track delegation chain
|
|
233
|
+
call_stack.push(@delegate_target)
|
|
234
|
+
begin
|
|
235
|
+
# Execute sub-swarm's lead agent
|
|
236
|
+
lead_agent = subswarm.agents[subswarm.lead_agent]
|
|
237
|
+
response = lead_agent.ask(message, source: "delegation")
|
|
238
|
+
result = response.content
|
|
239
|
+
|
|
240
|
+
# Reset if keep_context: false
|
|
241
|
+
swarm_registry.reset_if_needed(@delegate_target)
|
|
242
|
+
|
|
243
|
+
result
|
|
244
|
+
ensure
|
|
245
|
+
# Always pop from stack, even if delegation fails
|
|
246
|
+
call_stack.pop
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Emit circular dependency warning event
|
|
251
|
+
#
|
|
252
|
+
# @param call_stack [Array] Current delegation call stack
|
|
253
|
+
# @return [void]
|
|
254
|
+
def emit_circular_warning(call_stack)
|
|
255
|
+
LogStream.emit(
|
|
256
|
+
type: "delegation_circular_dependency",
|
|
257
|
+
agent: @agent_name,
|
|
258
|
+
swarm_id: @swarm.swarm_id,
|
|
259
|
+
parent_swarm_id: @swarm.parent_swarm_id,
|
|
260
|
+
target: @delegate_target,
|
|
261
|
+
call_stack: call_stack,
|
|
262
|
+
timestamp: Time.now.utc.iso8601,
|
|
263
|
+
)
|
|
264
|
+
end
|
|
162
265
|
end
|
|
163
266
|
end
|
|
164
267
|
end
|
data/lib/swarm_sdk/tools/edit.rb
CHANGED
|
@@ -10,6 +10,13 @@ module SwarmSDK
|
|
|
10
10
|
class Edit < RubyLLM::Tool
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
14
|
+
class << self
|
|
15
|
+
def creation_requirements
|
|
16
|
+
[:agent_name, :directory]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
description <<~DESC
|
|
14
21
|
Performs exact string replacements in files.
|
|
15
22
|
You must use your Read tool at least once in the conversation before editing.
|
|
@@ -55,8 +62,7 @@ module SwarmSDK
|
|
|
55
62
|
# @param directory [String] Agent's working directory
|
|
56
63
|
def initialize(agent_name:, directory:)
|
|
57
64
|
super()
|
|
58
|
-
|
|
59
|
-
@directory = File.expand_path(directory)
|
|
65
|
+
initialize_agent_context(agent_name: agent_name, directory: directory)
|
|
60
66
|
end
|
|
61
67
|
|
|
62
68
|
# Override name to return simple "Edit" instead of full class path
|
|
@@ -134,17 +140,6 @@ module SwarmSDK
|
|
|
134
140
|
rescue StandardError => e
|
|
135
141
|
error("Unexpected error editing file: #{e.class.name} - #{e.message}")
|
|
136
142
|
end
|
|
137
|
-
|
|
138
|
-
private
|
|
139
|
-
|
|
140
|
-
# Helper methods
|
|
141
|
-
def validation_error(message)
|
|
142
|
-
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def error(message)
|
|
146
|
-
"Error: #{message}"
|
|
147
|
-
end
|
|
148
143
|
end
|
|
149
144
|
end
|
|
150
145
|
end
|
data/lib/swarm_sdk/tools/glob.rb
CHANGED
|
@@ -9,6 +9,13 @@ module SwarmSDK
|
|
|
9
9
|
class Glob < RubyLLM::Tool
|
|
10
10
|
include PathResolver
|
|
11
11
|
|
|
12
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
13
|
+
class << self
|
|
14
|
+
def creation_requirements
|
|
15
|
+
[:directory]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def initialize(directory:)
|
|
13
20
|
super()
|
|
14
21
|
@directory = File.expand_path(directory)
|
|
@@ -43,7 +50,8 @@ module SwarmSDK
|
|
|
43
50
|
desc: "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.",
|
|
44
51
|
required: false
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
# Backward compatibility alias - use Defaults module for new code
|
|
54
|
+
MAX_RESULTS = Defaults::Limits::GLOB_RESULTS
|
|
47
55
|
|
|
48
56
|
def execute(pattern:, path: nil)
|
|
49
57
|
# Validate inputs
|
data/lib/swarm_sdk/tools/grep.rb
CHANGED
|
@@ -9,6 +9,13 @@ module SwarmSDK
|
|
|
9
9
|
class Grep < RubyLLM::Tool
|
|
10
10
|
include PathResolver
|
|
11
11
|
|
|
12
|
+
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
13
|
+
class << self
|
|
14
|
+
def creation_requirements
|
|
15
|
+
[:directory]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def initialize(directory:)
|
|
13
20
|
super()
|
|
14
21
|
@directory = File.expand_path(directory)
|