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.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +30 -19
  5. data/lib/claude_swarm/mcp_generator.rb +5 -10
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/run.rb +2 -2
  12. data/lib/swarm_cli/config_loader.rb +14 -14
  13. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  14. data/lib/swarm_cli/interactive_repl.rb +11 -5
  15. data/lib/swarm_cli/ui/icons.rb +0 -23
  16. data/lib/swarm_cli/version.rb +1 -1
  17. data/lib/swarm_memory/adapters/base.rb +4 -4
  18. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  19. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  20. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  21. data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
  22. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  23. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  24. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  25. data/lib/swarm_memory/version.rb +1 -1
  26. data/lib/swarm_memory.rb +6 -1
  27. data/lib/swarm_sdk/agent/builder.rb +91 -0
  28. data/lib/swarm_sdk/agent/chat.rb +540 -925
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
  30. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  31. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
  32. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  33. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  34. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  35. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  36. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
  37. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  38. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  39. data/lib/swarm_sdk/agent/context.rb +8 -4
  40. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  41. data/lib/swarm_sdk/agent/definition.rb +79 -174
  42. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
  43. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  44. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  45. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  46. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  47. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  48. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  49. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  50. data/lib/swarm_sdk/configuration.rb +100 -261
  51. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  52. data/lib/swarm_sdk/context_compactor.rb +6 -11
  53. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  54. data/lib/swarm_sdk/context_management/context.rb +328 -0
  55. data/lib/swarm_sdk/defaults.rb +196 -0
  56. data/lib/swarm_sdk/events_to_messages.rb +199 -0
  57. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  58. data/lib/swarm_sdk/log_collector.rb +192 -16
  59. data/lib/swarm_sdk/log_stream.rb +66 -8
  60. data/lib/swarm_sdk/model_aliases.json +4 -1
  61. data/lib/swarm_sdk/node_context.rb +1 -1
  62. data/lib/swarm_sdk/observer/builder.rb +81 -0
  63. data/lib/swarm_sdk/observer/config.rb +45 -0
  64. data/lib/swarm_sdk/observer/manager.rb +236 -0
  65. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  66. data/lib/swarm_sdk/plugin.rb +93 -3
  67. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  68. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  69. data/lib/swarm_sdk/restore_result.rb +65 -0
  70. data/lib/swarm_sdk/snapshot.rb +156 -0
  71. data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
  72. data/lib/swarm_sdk/state_restorer.rb +476 -0
  73. data/lib/swarm_sdk/state_snapshot.rb +334 -0
  74. data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
  75. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  76. data/lib/swarm_sdk/swarm/builder.rb +69 -407
  77. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  78. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  79. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  80. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  81. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  82. data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
  83. data/lib/swarm_sdk/swarm.rb +366 -631
  84. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  85. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +127 -24
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +28 -18
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  96. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  97. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  98. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  99. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
  100. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  101. data/lib/swarm_sdk/tools/think.rb +4 -1
  102. data/lib/swarm_sdk/tools/todo_write.rb +27 -8
  103. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  104. data/lib/swarm_sdk/tools/write.rb +8 -13
  105. data/lib/swarm_sdk/utils.rb +18 -0
  106. data/lib/swarm_sdk/validation_result.rb +33 -0
  107. data/lib/swarm_sdk/version.rb +1 -1
  108. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
  109. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  110. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  111. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
  112. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  113. data/lib/swarm_sdk/workflow.rb +554 -0
  114. data/lib/swarm_sdk.rb +393 -22
  115. metadata +51 -16
  116. data/lib/swarm_memory/chat_extension.rb +0 -34
  117. data/lib/swarm_sdk/node_orchestrator.rb +0 -591
  118. 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
@@ -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
- DEFAULT_TIMEOUT_MS = 120_000 # 2 minutes
82
- MAX_TIMEOUT_MS = 600_000 # 10 minutes
83
- MAX_OUTPUT_LENGTH = 30_000 # characters
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 delegating tasks to other agents in the swarm
5
+ # Delegate tool for working with other agents in the swarm
6
6
  #
7
- # Creates agent-specific delegation tools (e.g., DelegateTaskToBackend)
8
- # that allow one agent to delegate work to another agent.
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 in the expected format: DelegateTaskTo[AgentName]
42
- @tool_name = "DelegateTaskTo#{delegate_name.to_s.capitalize}"
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
- "Delegate tasks to #{@delegate_name}. #{@delegate_description}"
63
+ "Work with #{@delegate_name} to delegate work, ask questions, or collaborate. #{@delegate_description}"
49
64
  end
50
65
 
51
- param :task,
66
+ param :message,
52
67
  type: "string",
53
- desc: "Task description for the agent",
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 task [String] Task to delegate
78
+ # @param message [String] Message to send to the agent
64
79
  # @return [String] Result from delegate agent or error message
65
- def execute(task:)
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
- task: task,
108
+ message: message,
82
109
  timestamp: Time.now.utc.iso8601,
83
110
  },
84
111
  )
85
112
 
86
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
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
- # Proceed with delegation
98
- response = @delegate_chat.ask(task)
99
- delegation_result = response.content
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
- task: task,
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
- backtrace: e.backtrace&.first(5) || [],
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
- backtrace: e.backtrace&.first(5) || [],
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
- backtrace: backtrace_array,
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
@@ -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
- @agent_name = agent_name.to_sym
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
@@ -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
- MAX_RESULTS = 1000 # Limit results to prevent overwhelming output
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
@@ -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)