swarm_sdk 2.1.3 → 2.2.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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/builder.rb +33 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
- data/lib/swarm_sdk/agent/chat.rb +198 -51
- data/lib/swarm_sdk/agent/context.rb +6 -2
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +14 -2
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
- data/lib/swarm_sdk/configuration.rb +387 -94
- data/lib/swarm_sdk/events_to_messages.rb +181 -0
- data/lib/swarm_sdk/log_collector.rb +31 -5
- data/lib/swarm_sdk/log_stream.rb +37 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node/agent_config.rb +33 -8
- data/lib/swarm_sdk/node/builder.rb +39 -18
- data/lib/swarm_sdk/node_orchestrator.rb +293 -26
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
- 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 +386 -0
- data/lib/swarm_sdk/state_restorer.rb +491 -0
- data/lib/swarm_sdk/state_snapshot.rb +369 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +208 -12
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
- data/lib/swarm_sdk/swarm.rb +337 -42
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/delegate.rb +92 -7
- data/lib/swarm_sdk/tools/read.rb +17 -5
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
- 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.rb +40 -8
- metadata +17 -6
- data/lib/swarm_sdk/mcp.rb +0 -16
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Reconstructs RubyLLM::Message objects from SwarmSDK event streams
|
|
5
|
+
#
|
|
6
|
+
# This class enables conversation replay and analysis from event logs.
|
|
7
|
+
# It uses timestamps to maintain chronological ordering of messages.
|
|
8
|
+
#
|
|
9
|
+
# ## Limitations
|
|
10
|
+
#
|
|
11
|
+
# This reconstructs ONLY conversation messages. It does NOT restore:
|
|
12
|
+
# - Context state (warning thresholds, compression, todowrite index)
|
|
13
|
+
# - Scratchpad contents
|
|
14
|
+
# - Read tracking information
|
|
15
|
+
# - Full swarm state
|
|
16
|
+
#
|
|
17
|
+
# For full state restoration, use StateSnapshot/StateRestorer or SnapshotFromEvents.
|
|
18
|
+
#
|
|
19
|
+
# ## Usage
|
|
20
|
+
#
|
|
21
|
+
# # Collect events during execution
|
|
22
|
+
# events = []
|
|
23
|
+
# swarm.execute("Build feature") do |event|
|
|
24
|
+
# events << event
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # Reconstruct conversation for an agent
|
|
28
|
+
# messages = SwarmSDK::EventsToMessages.reconstruct(events, agent: :backend)
|
|
29
|
+
#
|
|
30
|
+
# # View conversation
|
|
31
|
+
# messages.each do |msg|
|
|
32
|
+
# puts "[#{msg.role}] #{msg.content}"
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# ## Event Requirements
|
|
36
|
+
#
|
|
37
|
+
# Events must have:
|
|
38
|
+
# - `:timestamp` field (ISO 8601 format) for ordering
|
|
39
|
+
# - `:agent` field to filter by agent
|
|
40
|
+
# - `:type` field to identify event type
|
|
41
|
+
#
|
|
42
|
+
# Supported event types:
|
|
43
|
+
# - `user_prompt`: Reconstructs user message (prompt in metadata or top-level)
|
|
44
|
+
# - `agent_step`: Reconstructs assistant message with tool calls
|
|
45
|
+
# - `agent_stop`: Reconstructs final assistant message
|
|
46
|
+
# - `tool_result`: Reconstructs tool result message
|
|
47
|
+
class EventsToMessages
|
|
48
|
+
class << self
|
|
49
|
+
# Reconstruct messages for an agent from event stream
|
|
50
|
+
#
|
|
51
|
+
# @param events [Array<Hash>] Event stream with timestamps
|
|
52
|
+
# @param agent [Symbol, String] Agent name to reconstruct messages for
|
|
53
|
+
# @return [Array<RubyLLM::Message>] Reconstructed messages in chronological order
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# messages = EventsToMessages.reconstruct(events, agent: :backend)
|
|
57
|
+
# messages.each { |msg| puts msg.content }
|
|
58
|
+
def reconstruct(events, agent:)
|
|
59
|
+
new(events, agent).reconstruct
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Initialize reconstructor
|
|
64
|
+
#
|
|
65
|
+
# @param events [Array<Hash>] Event stream
|
|
66
|
+
# @param agent [Symbol, String] Agent name
|
|
67
|
+
def initialize(events, agent)
|
|
68
|
+
@events = events
|
|
69
|
+
@agent = agent.to_sym
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reconstruct messages from events
|
|
73
|
+
#
|
|
74
|
+
# Filters events by agent, sorts by timestamp, and converts to RubyLLM::Message objects.
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<RubyLLM::Message>] Reconstructed messages
|
|
77
|
+
def reconstruct
|
|
78
|
+
messages = []
|
|
79
|
+
|
|
80
|
+
# Filter events for this agent and sort by timestamp
|
|
81
|
+
agent_events = @events
|
|
82
|
+
.select { |e| normalize_agent(e[:agent]) == @agent }
|
|
83
|
+
.sort_by { |e| parse_timestamp(e[:timestamp]) }
|
|
84
|
+
|
|
85
|
+
agent_events.each do |event|
|
|
86
|
+
message = case event[:type]&.to_s
|
|
87
|
+
when "user_prompt"
|
|
88
|
+
reconstruct_user_message(event)
|
|
89
|
+
when "agent_step", "agent_stop"
|
|
90
|
+
reconstruct_assistant_message(event)
|
|
91
|
+
when "tool_result"
|
|
92
|
+
reconstruct_tool_result_message(event)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
messages << message if message
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
messages
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Reconstruct user message from user_prompt event
|
|
104
|
+
#
|
|
105
|
+
# Extracts prompt from metadata or top-level field.
|
|
106
|
+
#
|
|
107
|
+
# @param event [Hash] user_prompt event
|
|
108
|
+
# @return [RubyLLM::Message, nil] User message or nil if prompt not found
|
|
109
|
+
def reconstruct_user_message(event)
|
|
110
|
+
# Try to extract prompt from metadata (current location) or top-level (potential future location)
|
|
111
|
+
prompt = event.dig(:metadata, :prompt) || event[:prompt]
|
|
112
|
+
return unless prompt && !prompt.to_s.empty?
|
|
113
|
+
|
|
114
|
+
RubyLLM::Message.new(
|
|
115
|
+
role: :user,
|
|
116
|
+
content: prompt,
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Reconstruct assistant message from agent_step or agent_stop event
|
|
121
|
+
#
|
|
122
|
+
# Converts tool_calls array to hash format expected by RubyLLM.
|
|
123
|
+
#
|
|
124
|
+
# @param event [Hash] agent_step or agent_stop event
|
|
125
|
+
# @return [RubyLLM::Message] Assistant message
|
|
126
|
+
def reconstruct_assistant_message(event)
|
|
127
|
+
# Convert tool_calls array to hash (RubyLLM format)
|
|
128
|
+
# Events emit tool_calls as Array, but RubyLLM expects Hash<String, ToolCall>
|
|
129
|
+
tool_calls_hash = if event[:tool_calls] && !event[:tool_calls].empty?
|
|
130
|
+
event[:tool_calls].each_with_object({}) do |tc, hash|
|
|
131
|
+
hash[tc[:id].to_s] = RubyLLM::ToolCall.new(
|
|
132
|
+
id: tc[:id],
|
|
133
|
+
name: tc[:name],
|
|
134
|
+
arguments: tc[:arguments] || {},
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
RubyLLM::Message.new(
|
|
140
|
+
role: :assistant,
|
|
141
|
+
content: event[:content] || "",
|
|
142
|
+
tool_calls: tool_calls_hash,
|
|
143
|
+
input_tokens: event.dig(:usage, :input_tokens),
|
|
144
|
+
output_tokens: event.dig(:usage, :output_tokens),
|
|
145
|
+
model_id: event[:model],
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Reconstruct tool result message from tool_result event
|
|
150
|
+
#
|
|
151
|
+
# @param event [Hash] tool_result event
|
|
152
|
+
# @return [RubyLLM::Message] Tool result message
|
|
153
|
+
def reconstruct_tool_result_message(event)
|
|
154
|
+
RubyLLM::Message.new(
|
|
155
|
+
role: :tool,
|
|
156
|
+
content: event[:result].to_s,
|
|
157
|
+
tool_call_id: event[:tool_call_id],
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Parse timestamp string to Time object
|
|
162
|
+
#
|
|
163
|
+
# @param timestamp [String, nil] ISO 8601 timestamp
|
|
164
|
+
# @return [Time] Parsed time or epoch if nil/invalid
|
|
165
|
+
def parse_timestamp(timestamp)
|
|
166
|
+
return Time.at(0) unless timestamp
|
|
167
|
+
|
|
168
|
+
Time.parse(timestamp)
|
|
169
|
+
rescue ArgumentError
|
|
170
|
+
Time.at(0)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Normalize agent name to symbol
|
|
174
|
+
#
|
|
175
|
+
# @param agent [Symbol, String, nil] Agent name
|
|
176
|
+
# @return [Symbol] Normalized agent name
|
|
177
|
+
def normalize_agent(agent)
|
|
178
|
+
agent.to_s.to_sym
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -7,6 +7,20 @@ module SwarmSDK
|
|
|
7
7
|
# to user-registered callbacks. It's designed to be set as the LogStream
|
|
8
8
|
# emitter during swarm execution.
|
|
9
9
|
#
|
|
10
|
+
# ## Thread Safety for Multi-Threaded Environments (Puma, Sidekiq)
|
|
11
|
+
#
|
|
12
|
+
# Callbacks are stored in Fiber-local storage (Fiber[:log_callbacks]) instead
|
|
13
|
+
# of class instance variables. This ensures callbacks registered in the parent
|
|
14
|
+
# thread/fiber are accessible to child fibers created by Async reactor.
|
|
15
|
+
#
|
|
16
|
+
# Why: In Puma/Sidekiq, class instance variables (@callbacks) are thread-isolated
|
|
17
|
+
# and don't properly propagate to child fibers. Using Fiber-local storage ensures
|
|
18
|
+
# events emitted from within Async blocks can reach registered callbacks.
|
|
19
|
+
#
|
|
20
|
+
# Child fibers inherit parent fiber-local storage automatically, so events
|
|
21
|
+
# emitted from agent callbacks (on_tool_call, on_end_message, etc.) executing
|
|
22
|
+
# in child fibers can still reach the parent's registered callbacks.
|
|
23
|
+
#
|
|
10
24
|
# ## Usage
|
|
11
25
|
#
|
|
12
26
|
# # Register a callback (before execution starts)
|
|
@@ -24,19 +38,31 @@ module SwarmSDK
|
|
|
24
38
|
class << self
|
|
25
39
|
# Register a callback to receive log events
|
|
26
40
|
#
|
|
41
|
+
# Stores callback in Fiber-local storage to ensure accessibility
|
|
42
|
+
# from child fibers in multi-threaded environments.
|
|
43
|
+
#
|
|
27
44
|
# @yield [Hash] Log event entry
|
|
28
45
|
def on_log(&block)
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
Fiber[:log_callbacks] ||= []
|
|
47
|
+
Fiber[:log_callbacks] << block
|
|
31
48
|
end
|
|
32
49
|
|
|
33
50
|
# Emit an event to all registered callbacks
|
|
34
51
|
#
|
|
52
|
+
# Automatically adds a timestamp if one doesn't exist.
|
|
53
|
+
# Reads callbacks from Fiber-local storage to support multi-threaded execution.
|
|
54
|
+
#
|
|
35
55
|
# @param entry [Hash] Log event entry
|
|
36
56
|
# @return [void]
|
|
37
57
|
def emit(entry)
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
# Ensure timestamp exists (LogStream adds it, but direct calls might not)
|
|
59
|
+
# Use microsecond precision (6 digits) for proper event ordering
|
|
60
|
+
entry_with_timestamp = entry.key?(:timestamp) ? entry : entry.merge(timestamp: Time.now.utc.iso8601(6))
|
|
61
|
+
|
|
62
|
+
# Read callbacks from Fiber-local storage (set by on_log in parent fiber)
|
|
63
|
+
callbacks = Fiber[:log_callbacks] || []
|
|
64
|
+
callbacks.each do |callback|
|
|
65
|
+
callback.call(entry_with_timestamp)
|
|
40
66
|
end
|
|
41
67
|
end
|
|
42
68
|
|
|
@@ -44,7 +70,7 @@ module SwarmSDK
|
|
|
44
70
|
#
|
|
45
71
|
# @return [void]
|
|
46
72
|
def reset!
|
|
47
|
-
|
|
73
|
+
Fiber[:log_callbacks] = []
|
|
48
74
|
end
|
|
49
75
|
end
|
|
50
76
|
end
|
data/lib/swarm_sdk/log_stream.rb
CHANGED
|
@@ -16,9 +16,15 @@ module SwarmSDK
|
|
|
16
16
|
# message_count: 5
|
|
17
17
|
# )
|
|
18
18
|
#
|
|
19
|
-
# ##
|
|
19
|
+
# ## Thread Safety
|
|
20
20
|
#
|
|
21
|
-
# LogStream is
|
|
21
|
+
# LogStream is thread-safe and fiber-safe:
|
|
22
|
+
# - Uses Fiber storage for per-request isolation in multi-threaded servers (Puma, Sidekiq)
|
|
23
|
+
# - Each thread/request has its own emitter instance
|
|
24
|
+
# - Child fibers inherit the emitter from their parent fiber
|
|
25
|
+
# - No cross-thread contamination of log events
|
|
26
|
+
#
|
|
27
|
+
# Usage pattern:
|
|
22
28
|
# 1. Set emitter BEFORE starting Async execution
|
|
23
29
|
# 2. During Async execution, only emit() (reads emitter)
|
|
24
30
|
# 3. Each event includes agent context for identification
|
|
@@ -35,34 +41,57 @@ module SwarmSDK
|
|
|
35
41
|
# Emit a log event
|
|
36
42
|
#
|
|
37
43
|
# Adds timestamp and forwards to the registered emitter.
|
|
44
|
+
# Auto-injects execution_id, swarm_id, and parent_swarm_id from Fiber storage.
|
|
45
|
+
# Explicit values in data override auto-injected ones.
|
|
38
46
|
#
|
|
39
47
|
# @param data [Hash] Event data (type, agent, and event-specific fields)
|
|
40
48
|
# @return [void]
|
|
41
49
|
def emit(**data)
|
|
42
|
-
|
|
50
|
+
emitter = Fiber[:log_stream_emitter]
|
|
51
|
+
return unless emitter
|
|
52
|
+
|
|
53
|
+
# Auto-inject execution context from Fiber storage
|
|
54
|
+
# Explicit values in data override auto-injected ones
|
|
55
|
+
auto_injected = {
|
|
56
|
+
execution_id: Fiber[:execution_id],
|
|
57
|
+
swarm_id: Fiber[:swarm_id],
|
|
58
|
+
parent_swarm_id: Fiber[:parent_swarm_id],
|
|
59
|
+
}.compact
|
|
43
60
|
|
|
44
|
-
entry = data.merge(timestamp: Time.now.utc.iso8601).compact
|
|
61
|
+
entry = auto_injected.merge(data).merge(timestamp: Time.now.utc.iso8601(6)).compact
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
emitter.emit(entry)
|
|
47
64
|
end
|
|
48
65
|
|
|
49
66
|
# Set the emitter (for dependency injection in tests)
|
|
50
67
|
#
|
|
68
|
+
# Stores emitter in Fiber storage for thread-safe, per-request isolation.
|
|
69
|
+
#
|
|
51
70
|
# @param emitter [#emit] Object responding to emit(Hash)
|
|
52
|
-
|
|
71
|
+
# @return [void]
|
|
72
|
+
def emitter=(emitter)
|
|
73
|
+
Fiber[:log_stream_emitter] = emitter
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the current emitter
|
|
77
|
+
#
|
|
78
|
+
# @return [#emit, nil] Current emitter or nil if not set
|
|
79
|
+
def emitter
|
|
80
|
+
Fiber[:log_stream_emitter]
|
|
81
|
+
end
|
|
53
82
|
|
|
54
83
|
# Reset the emitter (for test cleanup)
|
|
55
84
|
#
|
|
56
85
|
# @return [void]
|
|
57
86
|
def reset!
|
|
58
|
-
|
|
87
|
+
Fiber[:log_stream_emitter] = nil
|
|
59
88
|
end
|
|
60
89
|
|
|
61
90
|
# Check if logging is enabled
|
|
62
91
|
#
|
|
63
92
|
# @return [Boolean] true if an emitter is configured
|
|
64
93
|
def enabled?
|
|
65
|
-
|
|
94
|
+
!Fiber[:log_stream_emitter].nil?
|
|
66
95
|
end
|
|
67
96
|
end
|
|
68
97
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sonnet": "claude-sonnet-4-5-20250929",
|
|
3
3
|
"opus": "claude-opus-4-1-20250805",
|
|
4
|
-
"haiku": "claude-haiku-4-5-20251001"
|
|
4
|
+
"haiku": "claude-haiku-4-5-20251001",
|
|
5
|
+
"claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
|
|
6
|
+
"claude-opus-4-1": "claude-opus-4-1-20250805",
|
|
7
|
+
"claude-haiku-4-5": "claude-haiku-4-5-20251001"
|
|
5
8
|
}
|
|
@@ -7,6 +7,7 @@ module SwarmSDK
|
|
|
7
7
|
# This class enables the chainable syntax:
|
|
8
8
|
# agent(:backend).delegates_to(:tester, :database)
|
|
9
9
|
# agent(:backend, reset_context: false) # Preserve context across nodes
|
|
10
|
+
# agent(:backend).tools(:Read, :Edit) # Override tools for this node
|
|
10
11
|
#
|
|
11
12
|
# @example Basic delegation
|
|
12
13
|
# agent(:backend).delegates_to(:tester)
|
|
@@ -16,6 +17,12 @@ module SwarmSDK
|
|
|
16
17
|
#
|
|
17
18
|
# @example Preserve agent context
|
|
18
19
|
# agent(:architect, reset_context: false)
|
|
20
|
+
#
|
|
21
|
+
# @example Override tools for this node
|
|
22
|
+
# agent(:backend).tools(:Read, :Think)
|
|
23
|
+
#
|
|
24
|
+
# @example Combine delegation and tool override
|
|
25
|
+
# agent(:backend).delegates_to(:tester).tools(:Read, :Edit, :Write)
|
|
19
26
|
class AgentConfig
|
|
20
27
|
attr_reader :agent_name
|
|
21
28
|
|
|
@@ -24,6 +31,7 @@ module SwarmSDK
|
|
|
24
31
|
@node_builder = node_builder
|
|
25
32
|
@delegates_to = []
|
|
26
33
|
@reset_context = reset_context
|
|
34
|
+
@tools = nil # nil means use global agent definition tools
|
|
27
35
|
@finalized = false
|
|
28
36
|
end
|
|
29
37
|
|
|
@@ -33,21 +41,38 @@ module SwarmSDK
|
|
|
33
41
|
# @return [self] For method chaining
|
|
34
42
|
def delegates_to(*agent_names)
|
|
35
43
|
@delegates_to = agent_names.map(&:to_sym)
|
|
36
|
-
|
|
44
|
+
update_registration
|
|
37
45
|
self
|
|
38
46
|
end
|
|
39
47
|
|
|
40
|
-
#
|
|
48
|
+
# Override tools for this agent in this node
|
|
49
|
+
#
|
|
50
|
+
# @param tool_names [Array<Symbol>] Tool names to use (overrides global agent definition)
|
|
51
|
+
# @return [self] For method chaining
|
|
41
52
|
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
53
|
+
# @example
|
|
54
|
+
# agent(:backend).tools(:Read, :Edit)
|
|
55
|
+
def tools(*tool_names)
|
|
56
|
+
@tools = tool_names.map(&:to_sym)
|
|
57
|
+
update_registration
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Update agent registration (called after each fluent method)
|
|
62
|
+
#
|
|
63
|
+
# Always updates the registration with current state.
|
|
64
|
+
# This allows chaining: .delegates_to(...).tools(...)
|
|
44
65
|
#
|
|
45
66
|
# @return [void]
|
|
46
|
-
def
|
|
47
|
-
|
|
67
|
+
def update_registration
|
|
68
|
+
@node_builder.register_agent(@agent_name, @delegates_to, @reset_context, @tools)
|
|
69
|
+
end
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
# Finalize agent configuration (backward compatibility)
|
|
72
|
+
#
|
|
73
|
+
# @return [void]
|
|
74
|
+
def finalize
|
|
75
|
+
update_registration
|
|
51
76
|
end
|
|
52
77
|
end
|
|
53
78
|
end
|
|
@@ -43,8 +43,8 @@ module SwarmSDK
|
|
|
43
43
|
|
|
44
44
|
# Configure an agent for this node
|
|
45
45
|
#
|
|
46
|
-
# Returns an AgentConfig object that supports fluent delegation syntax.
|
|
47
|
-
# If delegates_to
|
|
46
|
+
# Returns an AgentConfig object that supports fluent delegation and tool override syntax.
|
|
47
|
+
# If delegates_to/tools are not called, the agent uses global configuration.
|
|
48
48
|
#
|
|
49
49
|
# By default, agents get fresh context in each node (reset_context: true).
|
|
50
50
|
# Set reset_context: false to preserve conversation history across nodes.
|
|
@@ -61,12 +61,18 @@ module SwarmSDK
|
|
|
61
61
|
#
|
|
62
62
|
# @example Preserve context across nodes
|
|
63
63
|
# agent(:architect, reset_context: false)
|
|
64
|
+
#
|
|
65
|
+
# @example Override tools for this node
|
|
66
|
+
# agent(:backend).tools(:Read, :Think)
|
|
67
|
+
#
|
|
68
|
+
# @example Combine delegation and tools
|
|
69
|
+
# agent(:backend).delegates_to(:tester).tools(:Read, :Edit, :Write)
|
|
64
70
|
def agent(name, reset_context: true)
|
|
65
71
|
config = AgentConfig.new(name, self, reset_context: reset_context)
|
|
66
72
|
|
|
67
|
-
# Register immediately with empty delegation
|
|
68
|
-
# If delegates_to
|
|
69
|
-
register_agent(name, [], reset_context)
|
|
73
|
+
# Register immediately with empty delegation and no tool override
|
|
74
|
+
# If delegates_to/tools are called later, they will update this
|
|
75
|
+
register_agent(name, [], reset_context, nil)
|
|
70
76
|
|
|
71
77
|
config
|
|
72
78
|
end
|
|
@@ -76,18 +82,25 @@ module SwarmSDK
|
|
|
76
82
|
# @param agent_name [Symbol] Agent name
|
|
77
83
|
# @param delegates_to [Array<Symbol>] Delegation targets
|
|
78
84
|
# @param reset_context [Boolean] Whether to reset agent context
|
|
85
|
+
# @param tools [Array<Symbol>, nil] Tool override for this node (nil = use global)
|
|
79
86
|
# @return [void]
|
|
80
|
-
def register_agent(agent_name, delegates_to, reset_context = true)
|
|
87
|
+
def register_agent(agent_name, delegates_to, reset_context = true, tools = nil)
|
|
81
88
|
# Check if agent already registered
|
|
82
89
|
existing = @agent_configs.find { |ac| ac[:agent] == agent_name }
|
|
83
90
|
|
|
84
91
|
if existing
|
|
85
|
-
# Update delegation and
|
|
92
|
+
# Update delegation, reset_context, and tools (happens when methods are called after agent())
|
|
86
93
|
existing[:delegates_to] = delegates_to
|
|
87
94
|
existing[:reset_context] = reset_context
|
|
95
|
+
existing[:tools] = tools unless tools.nil?
|
|
88
96
|
else
|
|
89
97
|
# Add new agent configuration
|
|
90
|
-
@agent_configs << {
|
|
98
|
+
@agent_configs << {
|
|
99
|
+
agent: agent_name,
|
|
100
|
+
delegates_to: delegates_to,
|
|
101
|
+
reset_context: reset_context,
|
|
102
|
+
tools: tools,
|
|
103
|
+
}
|
|
91
104
|
end
|
|
92
105
|
end
|
|
93
106
|
|
|
@@ -154,32 +167,36 @@ module SwarmSDK
|
|
|
154
167
|
# "Implement based on:\nPlan: #{plan}\nDesign: #{design}"
|
|
155
168
|
# end
|
|
156
169
|
#
|
|
157
|
-
# @example Skip execution (caching)
|
|
170
|
+
# @example Skip execution (caching) - using return
|
|
158
171
|
# input do |ctx|
|
|
159
172
|
# cached = check_cache(ctx.content)
|
|
160
173
|
# return ctx.skip_execution(content: cached) if cached
|
|
161
174
|
# ctx.content
|
|
162
175
|
# end
|
|
163
176
|
#
|
|
164
|
-
# @example Halt workflow (validation)
|
|
177
|
+
# @example Halt workflow (validation) - using return
|
|
165
178
|
# input do |ctx|
|
|
166
179
|
# if ctx.content.length > 10000
|
|
167
|
-
# # Halt entire workflow
|
|
180
|
+
# # Halt entire workflow - return works safely!
|
|
168
181
|
# return ctx.halt_workflow(content: "ERROR: Input too long")
|
|
169
182
|
# end
|
|
170
183
|
# ctx.content
|
|
171
184
|
# end
|
|
172
185
|
#
|
|
173
|
-
# @example Jump to different node (conditional routing)
|
|
186
|
+
# @example Jump to different node (conditional routing) - using return
|
|
174
187
|
# input do |ctx|
|
|
175
188
|
# if ctx.content.include?("NEEDS_REVIEW")
|
|
176
|
-
# # Jump to review node instead
|
|
189
|
+
# # Jump to review node instead - return works safely!
|
|
177
190
|
# return ctx.goto_node(:review, content: ctx.content)
|
|
178
191
|
# end
|
|
179
192
|
# ctx.content
|
|
180
193
|
# end
|
|
194
|
+
#
|
|
195
|
+
# @note The input block is automatically converted to a lambda, which means
|
|
196
|
+
# return statements work safely and only exit the transformer, not the
|
|
197
|
+
# entire program. This allows natural control flow patterns.
|
|
181
198
|
def input(&block)
|
|
182
|
-
@input_transformer = block
|
|
199
|
+
@input_transformer = ProcHelpers.to_lambda(block)
|
|
183
200
|
end
|
|
184
201
|
|
|
185
202
|
# Set input transformer as bash command (YAML API)
|
|
@@ -234,22 +251,26 @@ module SwarmSDK
|
|
|
234
251
|
# "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
|
|
235
252
|
# end
|
|
236
253
|
#
|
|
237
|
-
# @example Halt workflow (convergence check)
|
|
254
|
+
# @example Halt workflow (convergence check) - using return
|
|
238
255
|
# output do |ctx|
|
|
239
256
|
# return ctx.halt_workflow(content: ctx.content) if converged?(ctx.content)
|
|
240
257
|
# ctx.content
|
|
241
258
|
# end
|
|
242
259
|
#
|
|
243
|
-
# @example Jump to different node (conditional routing)
|
|
260
|
+
# @example Jump to different node (conditional routing) - using return
|
|
244
261
|
# output do |ctx|
|
|
245
262
|
# if needs_revision?(ctx.content)
|
|
246
|
-
# # Go back to revision node
|
|
263
|
+
# # Go back to revision node - return works safely!
|
|
247
264
|
# return ctx.goto_node(:revision, content: ctx.content)
|
|
248
265
|
# end
|
|
249
266
|
# ctx.content
|
|
250
267
|
# end
|
|
268
|
+
#
|
|
269
|
+
# @note The output block is automatically converted to a lambda, which means
|
|
270
|
+
# return statements work safely and only exit the transformer, not the
|
|
271
|
+
# entire program. This allows natural control flow patterns.
|
|
251
272
|
def output(&block)
|
|
252
|
-
@output_transformer = block
|
|
273
|
+
@output_transformer = ProcHelpers.to_lambda(block)
|
|
253
274
|
end
|
|
254
275
|
|
|
255
276
|
# Set output transformer as bash command (YAML API)
|