swarm_sdk 2.2.0 → 2.4.0

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +58 -0
  3. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  4. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  5. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  6. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  7. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  8. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +262 -0
  9. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  10. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  11. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
  12. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  13. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  14. data/lib/swarm_sdk/agent/context.rb +1 -2
  15. data/lib/swarm_sdk/agent/definition.rb +66 -154
  16. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  17. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  18. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  19. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  20. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  21. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  22. data/lib/swarm_sdk/config.rb +301 -0
  23. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  24. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  25. data/lib/swarm_sdk/configuration.rb +65 -543
  26. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  27. data/lib/swarm_sdk/context_compactor.rb +6 -11
  28. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  29. data/lib/swarm_sdk/context_management/context.rb +328 -0
  30. data/lib/swarm_sdk/defaults.rb +196 -0
  31. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  32. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  33. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
  34. data/lib/swarm_sdk/log_collector.rb +179 -29
  35. data/lib/swarm_sdk/log_stream.rb +29 -0
  36. data/lib/swarm_sdk/models.json +4333 -1
  37. data/lib/swarm_sdk/node_context.rb +1 -1
  38. data/lib/swarm_sdk/observer/builder.rb +81 -0
  39. data/lib/swarm_sdk/observer/config.rb +45 -0
  40. data/lib/swarm_sdk/observer/manager.rb +236 -0
  41. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  42. data/lib/swarm_sdk/plugin.rb +93 -3
  43. data/lib/swarm_sdk/snapshot.rb +6 -6
  44. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  45. data/lib/swarm_sdk/state_restorer.rb +136 -151
  46. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  47. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  48. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  49. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  50. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  51. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  52. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  53. data/lib/swarm_sdk/swarm/tool_configurator.rb +44 -140
  54. data/lib/swarm_sdk/swarm.rb +146 -689
  55. data/lib/swarm_sdk/tools/bash.rb +14 -8
  56. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  57. data/lib/swarm_sdk/tools/edit.rb +8 -13
  58. data/lib/swarm_sdk/tools/glob.rb +12 -4
  59. data/lib/swarm_sdk/tools/grep.rb +7 -0
  60. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  61. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  62. data/lib/swarm_sdk/tools/read.rb +16 -18
  63. data/lib/swarm_sdk/tools/registry.rb +122 -10
  64. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
  65. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  66. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  67. data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
  68. data/lib/swarm_sdk/tools/write.rb +8 -13
  69. data/lib/swarm_sdk/version.rb +1 -1
  70. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  71. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  72. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  73. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
  74. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
  75. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  76. data/lib/swarm_sdk.rb +64 -104
  77. metadata +68 -15
  78. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
@@ -5,24 +5,51 @@ module SwarmSDK
5
5
  # Registry for built-in SwarmSDK tools
6
6
  #
7
7
  # Maps tool names (symbols) to their RubyLLM::Tool classes.
8
- # Provides validation and lookup functionality for tool registration.
8
+ # Provides validation, lookup, and factory functionality for tool registration.
9
+ #
10
+ # ## Tool Creation Pattern
11
+ #
12
+ # Tools register themselves with their creation requirements via the `tool_factory` method.
13
+ # This eliminates the need for a giant case statement in ToolConfigurator.
14
+ #
15
+ # Tools fall into three categories:
16
+ # 1. **No params**: Simple tools with no initialization requirements (Think, Clock)
17
+ # 2. **Directory only**: Tools needing working directory (Bash, Grep, Glob)
18
+ # 3. **Agent context**: Tools needing agent tracking (Read, Write, Edit, MultiEdit)
19
+ # 4. **Scratchpad**: Tools needing scratchpad storage instance
20
+ #
21
+ # @example Adding a new tool with creation requirements
22
+ # # In the tool class:
23
+ # class MyTool < RubyLLM::Tool
24
+ # def self.creation_requirements
25
+ # [:agent_name, :directory]
26
+ # end
27
+ # end
28
+ #
29
+ # # In registry:
30
+ # BUILTIN_TOOLS = {
31
+ # MyTool: SwarmSDK::Tools::MyTool,
32
+ # }
9
33
  #
10
34
  # Note: Plugin-provided tools (e.g., memory tools) are NOT in this registry.
11
35
  # They are registered via SwarmSDK::PluginRegistry instead.
12
36
  class Registry
13
37
  # All available built-in tools
38
+ #
39
+ # Maps tool names to their classes. The class must respond to `creation_requirements`
40
+ # to specify what parameters are needed for instantiation.
14
41
  BUILTIN_TOOLS = {
15
- Read: :special, # Requires agent context for read tracking
16
- Write: :special, # Requires agent context for read-before-write enforcement
17
- Edit: :special, # Requires agent context for read-before-edit enforcement
42
+ Read: SwarmSDK::Tools::Read,
43
+ Write: SwarmSDK::Tools::Write,
44
+ Edit: SwarmSDK::Tools::Edit,
45
+ MultiEdit: SwarmSDK::Tools::MultiEdit,
18
46
  Bash: SwarmSDK::Tools::Bash,
19
47
  Grep: SwarmSDK::Tools::Grep,
20
48
  Glob: SwarmSDK::Tools::Glob,
21
- MultiEdit: :special, # Requires agent context for read-before-edit enforcement
22
- TodoWrite: :special, # Requires agent context for todo tracking
23
- ScratchpadWrite: :special, # Requires scratchpad storage instance
24
- ScratchpadRead: :special, # Requires scratchpad storage instance
25
- ScratchpadList: :special, # Requires scratchpad storage instance
49
+ TodoWrite: SwarmSDK::Tools::TodoWrite,
50
+ ScratchpadWrite: :scratchpad, # Requires scratchpad storage instance
51
+ ScratchpadRead: :scratchpad, # Requires scratchpad storage instance
52
+ ScratchpadList: :scratchpad, # Requires scratchpad storage instance
26
53
  Think: SwarmSDK::Tools::Think,
27
54
  WebFetch: SwarmSDK::Tools::WebFetch,
28
55
  Clock: SwarmSDK::Tools::Clock,
@@ -35,12 +62,49 @@ module SwarmSDK
35
62
  # They are managed by SwarmSDK::PluginRegistry instead.
36
63
  #
37
64
  # @param name [Symbol, String] Tool name
38
- # @return [Class, Symbol, nil] Tool class, :special, or nil if not found
65
+ # @return [Class, Symbol, nil] Tool class, :scratchpad marker, or nil if not found
39
66
  def get(name)
40
67
  name_sym = name.to_sym
41
68
  BUILTIN_TOOLS[name_sym]
42
69
  end
43
70
 
71
+ # Create a tool instance using the Factory Pattern
72
+ #
73
+ # Uses the tool's `creation_requirements` class method to determine
74
+ # what parameters to pass to the constructor.
75
+ #
76
+ # @param name [Symbol, String] Tool name
77
+ # @param context [Hash] Available context for tool creation
78
+ # @option context [Symbol] :agent_name Agent identifier
79
+ # @option context [String] :directory Agent's working directory
80
+ # @option context [Object] :scratchpad_storage Scratchpad storage instance
81
+ # @return [RubyLLM::Tool] Instantiated tool
82
+ # @raise [ConfigurationError] If tool is unknown or has unmet requirements
83
+ def create(name, context = {})
84
+ name_sym = name.to_sym
85
+ tool_entry = BUILTIN_TOOLS[name_sym]
86
+
87
+ raise ConfigurationError, "Unknown tool: #{name}" unless tool_entry
88
+
89
+ # Handle scratchpad tools specially (they use factory methods)
90
+ if tool_entry == :scratchpad
91
+ return create_scratchpad_tool(name_sym, context[:scratchpad_storage])
92
+ end
93
+
94
+ # Get the tool class and its requirements
95
+ tool_class = tool_entry
96
+
97
+ # Check if tool defines creation requirements
98
+ if tool_class.respond_to?(:creation_requirements)
99
+ requirements = tool_class.creation_requirements
100
+ params = extract_params(requirements, context, name)
101
+ tool_class.new(**params)
102
+ else
103
+ # No requirements - simple instantiation
104
+ tool_class.new
105
+ end
106
+ end
107
+
44
108
  # Get multiple tool classes by names
45
109
  #
46
110
  # @param names [Array<Symbol, String>] Tool names
@@ -87,6 +151,54 @@ module SwarmSDK
87
151
  def validate(names)
88
152
  names.reject { |name| exists?(name) }
89
153
  end
154
+
155
+ private
156
+
157
+ # Extract required parameters from context
158
+ #
159
+ # @param requirements [Array<Symbol>] Required parameter names
160
+ # @param context [Hash] Available context
161
+ # @param tool_name [Symbol] Tool name for error messages
162
+ # @return [Hash] Parameters to pass to tool constructor
163
+ # @raise [ConfigurationError] If required parameter is missing
164
+ def extract_params(requirements, context, tool_name)
165
+ params = {}
166
+
167
+ requirements.each do |req|
168
+ unless context.key?(req)
169
+ raise ConfigurationError,
170
+ "Tool #{tool_name} requires #{req} but it was not provided in context"
171
+ end
172
+
173
+ params[req] = context[req]
174
+ end
175
+
176
+ params
177
+ end
178
+
179
+ # Create a scratchpad tool using its factory method
180
+ #
181
+ # @param name [Symbol] Scratchpad tool name
182
+ # @param storage [Object] Scratchpad storage instance
183
+ # @return [RubyLLM::Tool] Instantiated scratchpad tool
184
+ # @raise [ConfigurationError] If storage is not provided
185
+ def create_scratchpad_tool(name, storage)
186
+ unless storage
187
+ raise ConfigurationError,
188
+ "Scratchpad tool #{name} requires scratchpad_storage in context"
189
+ end
190
+
191
+ case name
192
+ when :ScratchpadWrite
193
+ Tools::Scratchpad::ScratchpadWrite.create_for_scratchpad(storage)
194
+ when :ScratchpadRead
195
+ Tools::Scratchpad::ScratchpadRead.create_for_scratchpad(storage)
196
+ when :ScratchpadList
197
+ Tools::Scratchpad::ScratchpadList.create_for_scratchpad(storage)
198
+ else
199
+ raise ConfigurationError, "Unknown scratchpad tool: #{name}"
200
+ end
201
+ end
90
202
  end
91
203
  end
92
204
  end
@@ -15,10 +15,13 @@ module SwarmSDK
15
15
  # Use for temporary, cross-agent communication within a single session.
16
16
  class ScratchpadStorage < Storage
17
17
  # Initialize scratchpad storage (always volatile)
18
- def initialize
18
+ #
19
+ # @param total_size_limit [Integer, nil] Maximum total size in bytes (defaults to Defaults::Storage::TOTAL_SIZE_BYTES)
20
+ def initialize(total_size_limit: nil)
19
21
  super() # Initialize parent Storage class
20
22
  @entries = {}
21
23
  @total_size = 0
24
+ @total_size_limit = total_size_limit || SwarmSDK.config.scratchpad_total_size_limit
22
25
  @mutex = Mutex.new
23
26
  end
24
27
 
@@ -38,8 +41,9 @@ module SwarmSDK
38
41
  content_size = content.bytesize
39
42
 
40
43
  # Check entry size limit
41
- if content_size > MAX_ENTRY_SIZE
42
- raise ArgumentError, "Content exceeds maximum size (#{format_bytes(MAX_ENTRY_SIZE)}). " \
44
+ entry_size_limit = SwarmSDK.config.scratchpad_entry_size_limit
45
+ if content_size > entry_size_limit
46
+ raise ArgumentError, "Content exceeds maximum size (#{format_bytes(entry_size_limit)}). " \
43
47
  "Current: #{format_bytes(content_size)}"
44
48
  end
45
49
 
@@ -49,8 +53,8 @@ module SwarmSDK
49
53
  new_total_size = @total_size - existing_size + content_size
50
54
 
51
55
  # Check total size limit
52
- if new_total_size > MAX_TOTAL_SIZE
53
- raise ArgumentError, "Scratchpad full (#{format_bytes(MAX_TOTAL_SIZE)} limit). " \
56
+ if new_total_size > @total_size_limit
57
+ raise ArgumentError, "Scratchpad full (#{format_bytes(@total_size_limit)} limit). " \
54
58
  "Current: #{format_bytes(@total_size)}, " \
55
59
  "Would be: #{format_bytes(new_total_size)}. " \
56
60
  "Clear old entries or use smaller content."
@@ -14,12 +14,6 @@ module SwarmSDK
14
14
  # - Search capabilities: Glob patterns and grep-style content search
15
15
  # - Thread-safe: Mutex-protected operations
16
16
  class Storage
17
- # Maximum size per entry (3MB)
18
- MAX_ENTRY_SIZE = 3_000_000
19
-
20
- # Maximum total storage size (100GB)
21
- MAX_TOTAL_SIZE = 100_000_000_000
22
-
23
17
  # Represents a single storage entry with metadata
24
18
  Entry = Struct.new(:content, :title, :updated_at, :size, keyword_init: true)
25
19
 
@@ -7,6 +7,13 @@ module SwarmSDK
7
7
  # This tool helps agents track progress on complex multi-step tasks.
8
8
  # Each agent maintains its own independent todo list.
9
9
  class TodoWrite < RubyLLM::Tool
10
+ # Factory pattern: declare what parameters this tool needs for instantiation
11
+ class << self
12
+ def creation_requirements
13
+ [:agent_name]
14
+ end
15
+ end
16
+
10
17
  description <<~DESC
11
18
  Use this tool to create and manage a structured task list for your current work session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
12
19
  It also helps the user understand the progress of the task and overall progress of their requests.
@@ -11,7 +11,6 @@ module SwarmSDK
11
11
  super()
12
12
  @cache = {}
13
13
  @cache_ttl = 900 # 15 minutes in seconds
14
- @llm_enabled = SwarmSDK.settings.webfetch_llm_enabled?
15
14
  end
16
15
 
17
16
  def name
@@ -51,16 +50,18 @@ module SwarmSDK
51
50
  desc: "The prompt to run on the fetched content. Required when SwarmSDK is configured with webfetch_provider and webfetch_model. Optional otherwise (ignored if LLM processing not configured).",
52
51
  required: false
53
52
 
54
- MAX_CONTENT_LENGTH = 100_000 # characters
53
+ # NOTE: Content length and timeout now accessed via SwarmSDK.config
55
54
  USER_AGENT = "SwarmSDK WebFetch Tool (https://github.com/parruda/claude-swarm)"
56
- TIMEOUT = 30 # seconds
57
55
 
58
56
  def execute(url:, prompt: nil)
59
57
  # Validate inputs
60
58
  return validation_error("url is required") if url.nil? || url.empty?
61
59
 
60
+ # Check if LLM processing is enabled (lazy check)
61
+ llm_enabled = SwarmSDK.config.webfetch_llm_enabled?
62
+
62
63
  # Validate prompt when LLM processing is enabled
63
- if @llm_enabled && (prompt.nil? || prompt.empty?)
64
+ if llm_enabled && (prompt.nil? || prompt.empty?)
64
65
  return validation_error("prompt is required when LLM processing is configured")
65
66
  end
66
67
 
@@ -69,7 +70,7 @@ module SwarmSDK
69
70
  return validation_error("Invalid URL format: #{url}") unless normalized_url
70
71
 
71
72
  # Check cache first (cache key includes prompt if LLM is enabled)
72
- cache_key = @llm_enabled ? "#{normalized_url}:#{prompt}" : normalized_url
73
+ cache_key = llm_enabled ? "#{normalized_url}:#{prompt}" : normalized_url
73
74
  cached = get_from_cache(cache_key)
74
75
  return cached if cached
75
76
 
@@ -86,13 +87,14 @@ module SwarmSDK
86
87
  markdown_content = html_to_markdown(fetch_result[:body])
87
88
 
88
89
  # Truncate if too long
89
- if markdown_content.length > MAX_CONTENT_LENGTH
90
- markdown_content = markdown_content[0...MAX_CONTENT_LENGTH]
90
+ max_content = SwarmSDK.config.web_fetch_character_limit
91
+ if markdown_content.length > max_content
92
+ markdown_content = markdown_content[0...max_content]
91
93
  markdown_content += "\n\n[Content truncated due to length]"
92
94
  end
93
95
 
94
96
  # Process with AI model if LLM is enabled, otherwise return markdown
95
- result = if @llm_enabled
97
+ result = if llm_enabled
96
98
  process_with_llm(markdown_content, prompt, normalized_url)
97
99
  else
98
100
  markdown_content
@@ -142,12 +144,13 @@ module SwarmSDK
142
144
  require "faraday"
143
145
  require "faraday/follow_redirects"
144
146
 
147
+ timeout = SwarmSDK.config.web_fetch_timeout
145
148
  response = Faraday.new(url: url) do |conn|
146
149
  conn.request(:url_encoded)
147
150
  conn.response(:follow_redirects, limit: 5)
148
151
  conn.adapter(Faraday.default_adapter)
149
- conn.options.timeout = TIMEOUT
150
- conn.options.open_timeout = TIMEOUT
152
+ conn.options.timeout = timeout
153
+ conn.options.open_timeout = timeout
151
154
  end.get do |req|
152
155
  req.headers["User-Agent"] = USER_AGENT
153
156
  req.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
@@ -166,7 +169,7 @@ module SwarmSDK
166
169
  redirect_url: redirect_url,
167
170
  }
168
171
  rescue Faraday::TimeoutError
169
- error("Request timed out after #{TIMEOUT} seconds")
172
+ error("Request timed out after #{SwarmSDK.config.web_fetch_timeout} seconds")
170
173
  rescue Faraday::ConnectionFailed => e
171
174
  error("Connection failed: #{e.message}")
172
175
  rescue StandardError => e
@@ -193,17 +196,17 @@ module SwarmSDK
193
196
  Please respond to the user's request based on the content above.
194
197
  PROMPT
195
198
 
196
- # Get settings
197
- config = SwarmSDK.settings
199
+ # Get config
200
+ sdk_config = SwarmSDK.config
198
201
 
199
202
  # Build chat with configured provider and model
200
203
  chat_params = {
201
- model: config.webfetch_model,
202
- provider: config.webfetch_provider.to_sym,
204
+ model: sdk_config.webfetch_model,
205
+ provider: sdk_config.webfetch_provider.to_sym,
203
206
  }
204
- chat_params[:base_url] = config.webfetch_base_url if config.webfetch_base_url
207
+ chat_params[:base_url] = sdk_config.webfetch_base_url if sdk_config.webfetch_base_url
205
208
 
206
- chat = RubyLLM.chat(**chat_params).with_params(max_tokens: config.webfetch_max_tokens)
209
+ chat = RubyLLM.chat(**chat_params).with_params(max_tokens: sdk_config.webfetch_max_tokens)
207
210
 
208
211
  response = chat.ask(full_prompt)
209
212
 
@@ -10,6 +10,13 @@ module SwarmSDK
10
10
  class Write < 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
  Writes a file to the local filesystem.
15
22
  This tool will overwrite the existing file if there is one at the provided path.
@@ -42,8 +49,7 @@ module SwarmSDK
42
49
  # @param directory [String] Agent's working directory
43
50
  def initialize(agent_name:, directory:)
44
51
  super()
45
- @agent_name = agent_name.to_sym
46
- @directory = File.expand_path(directory)
52
+ initialize_agent_context(agent_name: agent_name, directory: directory)
47
53
  end
48
54
 
49
55
  # Override name to return simple "Write" instead of full class path
@@ -101,17 +107,6 @@ module SwarmSDK
101
107
  rescue StandardError => e
102
108
  error("Unexpected error writing file: #{e.class.name} - #{e.message}")
103
109
  end
104
-
105
- private
106
-
107
- # Helper methods
108
- def validation_error(message)
109
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
110
- end
111
-
112
- def error(message)
113
- "Error: #{message}"
114
- end
115
110
  end
116
111
  end
117
112
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.2.0"
4
+ VERSION = "2.4.0"
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- module Node
4
+ class Workflow
5
5
  # AgentConfig provides fluent API for configuring agents within a node
6
6
  #
7
7
  # This class enables the chainable syntax:
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ class Workflow
5
+ # Builder provides DSL for building multi-node workflows
6
+ # This is the top-level builder accessed via SwarmSDK.workflow
7
+ #
8
+ # The DSL enables:
9
+ # - Node-based workflow configuration
10
+ # - Agent delegation per node
11
+ # - Input/output transformers for data flow
12
+ # - Context preservation across nodes
13
+ #
14
+ # @example Multi-stage workflow
15
+ # workflow = SwarmSDK.workflow do
16
+ # name "Build Pipeline"
17
+ # start_node :planning
18
+ #
19
+ # agent :architect do
20
+ # model "gpt-5"
21
+ # prompt "You design systems"
22
+ # end
23
+ #
24
+ # agent :coder do
25
+ # model "gpt-4"
26
+ # prompt "You implement code"
27
+ # end
28
+ #
29
+ # node :planning do
30
+ # agent(:architect)
31
+ # end
32
+ #
33
+ # node :implementation do
34
+ # agent(:coder)
35
+ # depends_on :planning
36
+ # end
37
+ # end
38
+ #
39
+ # workflow.execute("Build auth system")
40
+ class Builder < Builders::BaseBuilder
41
+ # Main entry point for DSL
42
+ #
43
+ # @example
44
+ # workflow = SwarmSDK.workflow do
45
+ # name "Pipeline"
46
+ # start_node :planning
47
+ # node(:planning) { agent(:architect) }
48
+ # end
49
+ class << self
50
+ def build(allow_filesystem_tools: nil, &block)
51
+ builder = new(allow_filesystem_tools: allow_filesystem_tools)
52
+ builder.instance_eval(&block)
53
+ builder.build_swarm
54
+ end
55
+ end
56
+
57
+ def initialize(allow_filesystem_tools: nil)
58
+ super
59
+ @nodes = {}
60
+ @start_node = nil
61
+ end
62
+
63
+ # Define a node (mini-swarm execution stage)
64
+ #
65
+ # Nodes enable multi-stage workflows where different agent teams
66
+ # collaborate in sequence. Each node is an independent swarm execution.
67
+ #
68
+ # @param name [Symbol] Node name
69
+ # @yield Block for node configuration
70
+ # @return [void]
71
+ #
72
+ # @example Solo agent node
73
+ # node :planning do
74
+ # agent(:architect)
75
+ # end
76
+ #
77
+ # @example Multi-agent node with delegation
78
+ # node :implementation do
79
+ # agent(:backend).delegates_to(:tester, :database)
80
+ # agent(:tester).delegates_to(:database)
81
+ # agent(:database)
82
+ # depends_on :planning
83
+ # end
84
+ def node(name, &block)
85
+ builder = Workflow::NodeBuilder.new(name)
86
+ builder.instance_eval(&block)
87
+ @nodes[name] = builder
88
+ end
89
+
90
+ # Set the starting node for workflow execution
91
+ #
92
+ # Required when nodes are defined. Specifies which node to execute first.
93
+ #
94
+ # @param name [Symbol] Name of starting node
95
+ # @return [void]
96
+ #
97
+ # @example
98
+ # start_node :planning
99
+ def start_node(name)
100
+ @start_node = name.to_sym
101
+ end
102
+
103
+ # Build the actual Workflow instance
104
+ def build_swarm # Returns Workflow despite method name
105
+ raise ConfigurationError, "Workflow name not set. Use: name 'My Workflow'" unless @swarm_name
106
+ raise ConfigurationError, "No nodes defined. Use: node :name { ... }" if @nodes.empty?
107
+ raise ConfigurationError, "start_node not set. Use: start_node :name" unless @start_node
108
+
109
+ # Validate filesystem tools BEFORE building
110
+ validate_all_agents_filesystem_tools if @all_agents_config
111
+ validate_agent_filesystem_tools
112
+
113
+ build_workflow
114
+ end
115
+
116
+ private
117
+
118
+ # Build a node-based workflow
119
+ #
120
+ # @return [Workflow] Configured workflow instance
121
+ def build_workflow
122
+ # Build agent definitions
123
+ agent_definitions = build_agent_definitions
124
+
125
+ # Create workflow
126
+ workflow = Workflow.new(
127
+ swarm_name: @swarm_name,
128
+ swarm_id: @swarm_id,
129
+ agent_definitions: agent_definitions,
130
+ nodes: @nodes,
131
+ start_node: @start_node,
132
+ scratchpad: @scratchpad,
133
+ allow_filesystem_tools: @allow_filesystem_tools,
134
+ )
135
+
136
+ # Pass swarm registry config to workflow if external swarms registered
137
+ workflow.swarm_registry_config = @swarm_registry_config if @swarm_registry_config.any?
138
+
139
+ workflow
140
+ end
141
+ end
142
+ end
143
+ end