swarm_memory 2.1.3 → 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 +2 -15
- data/lib/claude_swarm/mcp_generator.rb +1 -0
- 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/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +11 -11
- 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/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +1 -1
- data/lib/swarm_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1059
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
- 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 +12 -12
- 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 +2 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- 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 +65 -543
- 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 +18 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- 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/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- 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/tool_configurator.rb +42 -138
- data/lib/swarm_sdk/swarm.rb +137 -679
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- 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 +11 -13
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- 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} +3 -3
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +33 -3
- metadata +37 -14
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class Workflow
|
|
5
|
+
# Handles workflow execution orchestration
|
|
6
|
+
#
|
|
7
|
+
# Extracted from Workflow#execute to reduce complexity and improve maintainability.
|
|
8
|
+
# Orchestrates node execution, transformer handling, and control flow.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# executor = Executor.new(workflow)
|
|
12
|
+
# result = executor.run("Build auth system") { |entry| puts entry }
|
|
13
|
+
class Executor
|
|
14
|
+
# Execution state container
|
|
15
|
+
#
|
|
16
|
+
# Holds mutable state during workflow execution to avoid instance variable pollution.
|
|
17
|
+
ExecutionState = Struct.new(
|
|
18
|
+
:current_input,
|
|
19
|
+
:results,
|
|
20
|
+
:last_result,
|
|
21
|
+
:execution_index,
|
|
22
|
+
:logs,
|
|
23
|
+
keyword_init: true,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Output transformer context container
|
|
27
|
+
#
|
|
28
|
+
# Groups parameters for output transformer processing to avoid long parameter lists.
|
|
29
|
+
OutputTransformerContext = Struct.new(
|
|
30
|
+
:node,
|
|
31
|
+
:node_name,
|
|
32
|
+
:node_start_time,
|
|
33
|
+
:state,
|
|
34
|
+
:result,
|
|
35
|
+
:skip_execution,
|
|
36
|
+
keyword_init: true,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def initialize(workflow)
|
|
40
|
+
@workflow = workflow
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Execute the workflow with a prompt
|
|
44
|
+
#
|
|
45
|
+
# @param prompt [String] Initial prompt for the workflow
|
|
46
|
+
# @param inherit_subscriptions [Boolean] Whether to inherit parent log subscriptions
|
|
47
|
+
# @yield [Hash] Log entry if block given (for streaming)
|
|
48
|
+
# @return [Result] Final result from last node execution
|
|
49
|
+
def run(prompt, inherit_subscriptions: true, &block)
|
|
50
|
+
@parent_subscriptions = capture_parent_subscriptions if inherit_subscriptions
|
|
51
|
+
setup_logging(inherit_subscriptions: inherit_subscriptions, &block)
|
|
52
|
+
setup_fiber_context
|
|
53
|
+
@workflow.original_prompt = prompt
|
|
54
|
+
|
|
55
|
+
state = ExecutionState.new(
|
|
56
|
+
current_input: prompt,
|
|
57
|
+
results: {},
|
|
58
|
+
last_result: nil,
|
|
59
|
+
execution_index: 0,
|
|
60
|
+
logs: [],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
execute_nodes(state)
|
|
64
|
+
ensure
|
|
65
|
+
cleanup_fiber_context
|
|
66
|
+
reset_logging
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Capture parent subscriptions before overwriting Fiber storage
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<LogCollector::Subscription>] Parent subscriptions
|
|
74
|
+
def capture_parent_subscriptions
|
|
75
|
+
Fiber[:log_subscriptions] || []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Setup logging infrastructure if block given
|
|
79
|
+
#
|
|
80
|
+
# @param inherit_subscriptions [Boolean] Whether to inherit parent subscriptions
|
|
81
|
+
# @yield [Hash] Log entry for streaming
|
|
82
|
+
# @return [void]
|
|
83
|
+
def setup_logging(inherit_subscriptions: true, &block)
|
|
84
|
+
@has_logging = block_given?
|
|
85
|
+
return unless @has_logging
|
|
86
|
+
|
|
87
|
+
Fiber[:log_subscriptions] = if inherit_subscriptions && @parent_subscriptions
|
|
88
|
+
# Keep parent subscriptions and add new one
|
|
89
|
+
@parent_subscriptions.dup
|
|
90
|
+
else
|
|
91
|
+
# Isolate: start with fresh subscriptions
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
LogCollector.subscribe do |entry|
|
|
96
|
+
block.call(entry)
|
|
97
|
+
end
|
|
98
|
+
LogStream.emitter = LogCollector
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Setup fiber-local execution context
|
|
102
|
+
#
|
|
103
|
+
# @return [void]
|
|
104
|
+
def setup_fiber_context
|
|
105
|
+
Fiber[:execution_id] = generate_execution_id
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Cleanup fiber-local storage
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def cleanup_fiber_context
|
|
112
|
+
Fiber[:execution_id] = nil
|
|
113
|
+
Fiber[:swarm_id] = nil
|
|
114
|
+
Fiber[:parent_swarm_id] = nil
|
|
115
|
+
Fiber[:log_subscriptions] = nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Reset logging state
|
|
119
|
+
#
|
|
120
|
+
# @return [void]
|
|
121
|
+
def reset_logging
|
|
122
|
+
return unless @has_logging
|
|
123
|
+
|
|
124
|
+
LogCollector.reset!
|
|
125
|
+
LogStream.reset!
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Generate unique execution ID for workflow
|
|
129
|
+
#
|
|
130
|
+
# @return [String] Generated execution ID
|
|
131
|
+
def generate_execution_id
|
|
132
|
+
"exec_workflow_#{SecureRandom.hex(8)}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Main node iteration loop with control flow support
|
|
136
|
+
#
|
|
137
|
+
# @param state [ExecutionState] Mutable execution state
|
|
138
|
+
# @return [Result] Final result
|
|
139
|
+
def execute_nodes(state)
|
|
140
|
+
while state.execution_index < @workflow.execution_order.size
|
|
141
|
+
control_action = execute_single_node(state)
|
|
142
|
+
|
|
143
|
+
case control_action[:action]
|
|
144
|
+
when :halt
|
|
145
|
+
return control_action[:result]
|
|
146
|
+
when :goto
|
|
147
|
+
state.execution_index = find_node_index(control_action[:target])
|
|
148
|
+
state.current_input = control_action[:content]
|
|
149
|
+
next
|
|
150
|
+
when :continue
|
|
151
|
+
state.execution_index += 1
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
state.last_result
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Execute a single node with full lifecycle
|
|
159
|
+
#
|
|
160
|
+
# @param state [ExecutionState] Mutable execution state
|
|
161
|
+
# @return [Hash] Control action (:halt, :goto, or :continue)
|
|
162
|
+
def execute_single_node(state)
|
|
163
|
+
node_name = @workflow.execution_order[state.execution_index]
|
|
164
|
+
node = @workflow.nodes[node_name]
|
|
165
|
+
node_start_time = Time.now
|
|
166
|
+
|
|
167
|
+
setup_node_fiber_context(node_name)
|
|
168
|
+
emit_node_start(node_name, node)
|
|
169
|
+
|
|
170
|
+
# Process input transformer (may modify current_input or return control flow)
|
|
171
|
+
input_result = process_input_transformer(node, node_name, node_start_time, state)
|
|
172
|
+
return input_result if input_result[:action] == :halt || input_result[:action] == :goto
|
|
173
|
+
|
|
174
|
+
skip_execution = input_result[:skip]
|
|
175
|
+
state.current_input = input_result[:content]
|
|
176
|
+
|
|
177
|
+
# Execute node (or skip if requested)
|
|
178
|
+
result = execute_node(node, node_name, state.current_input, skip_execution)
|
|
179
|
+
state.results[node_name] = result
|
|
180
|
+
state.last_result = result
|
|
181
|
+
|
|
182
|
+
log_node_error(node_name, result) if result.error
|
|
183
|
+
|
|
184
|
+
# Process output transformer (may return control flow)
|
|
185
|
+
ctx = OutputTransformerContext.new(
|
|
186
|
+
node: node,
|
|
187
|
+
node_name: node_name,
|
|
188
|
+
node_start_time: node_start_time,
|
|
189
|
+
state: state,
|
|
190
|
+
result: result,
|
|
191
|
+
skip_execution: skip_execution,
|
|
192
|
+
)
|
|
193
|
+
output_result = process_output_transformer(ctx)
|
|
194
|
+
|
|
195
|
+
case output_result[:action]
|
|
196
|
+
when :halt
|
|
197
|
+
return output_result
|
|
198
|
+
when :goto
|
|
199
|
+
emit_node_stop(node_name, node, result, Time.now - node_start_time, skip_execution)
|
|
200
|
+
return output_result
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
state.current_input = output_result[:content]
|
|
204
|
+
|
|
205
|
+
# Update result for agent-less nodes with transformed content
|
|
206
|
+
update_agentless_result(node, node_name, state, result)
|
|
207
|
+
|
|
208
|
+
emit_node_stop(node_name, node, result, Time.now - node_start_time, skip_execution)
|
|
209
|
+
|
|
210
|
+
{ action: :continue }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Setup fiber-local context for node execution
|
|
214
|
+
#
|
|
215
|
+
# @param node_name [Symbol] Node name
|
|
216
|
+
# @return [void]
|
|
217
|
+
def setup_node_fiber_context(node_name)
|
|
218
|
+
node_swarm_id = @workflow.swarm_id ? "#{@workflow.swarm_id}/node:#{node_name}" : nil
|
|
219
|
+
Fiber[:swarm_id] = node_swarm_id
|
|
220
|
+
Fiber[:parent_swarm_id] = @workflow.swarm_id
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Process input transformer and handle control flow
|
|
224
|
+
#
|
|
225
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
226
|
+
# @param node_name [Symbol] Node name
|
|
227
|
+
# @param node_start_time [Time] When node started
|
|
228
|
+
# @param state [ExecutionState] Current execution state
|
|
229
|
+
# @return [Hash] Control action with :skip and :content keys
|
|
230
|
+
def process_input_transformer(node, node_name, node_start_time, state)
|
|
231
|
+
unless node.has_input_transformer?
|
|
232
|
+
return { action: :continue, skip: false, content: state.current_input }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
input_context = build_input_context(node, node_name, state)
|
|
236
|
+
transformed = node.transform_input(input_context, current_input: state.current_input)
|
|
237
|
+
|
|
238
|
+
handle_input_control_flow(transformed, node_name, node, node_start_time)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Build NodeContext for input transformer
|
|
242
|
+
#
|
|
243
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
244
|
+
# @param node_name [Symbol] Node name
|
|
245
|
+
# @param state [ExecutionState] Current execution state
|
|
246
|
+
# @return [NodeContext] Context for transformer
|
|
247
|
+
def build_input_context(node, node_name, state)
|
|
248
|
+
previous_result = resolve_previous_result(node, state)
|
|
249
|
+
|
|
250
|
+
NodeContext.for_input(
|
|
251
|
+
previous_result: previous_result,
|
|
252
|
+
all_results: state.results,
|
|
253
|
+
original_prompt: @workflow.original_prompt,
|
|
254
|
+
node_name: node_name,
|
|
255
|
+
dependencies: node.dependencies,
|
|
256
|
+
transformed_content: node.dependencies.size == 1 ? state.current_input : nil,
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Resolve previous result based on dependencies
|
|
261
|
+
#
|
|
262
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
263
|
+
# @param state [ExecutionState] Current execution state
|
|
264
|
+
# @return [Result, Hash, String] Previous result(s) or prompt
|
|
265
|
+
def resolve_previous_result(node, state)
|
|
266
|
+
case node.dependencies.size
|
|
267
|
+
when 0
|
|
268
|
+
state.current_input
|
|
269
|
+
when 1
|
|
270
|
+
state.results[node.dependencies.first]
|
|
271
|
+
else
|
|
272
|
+
node.dependencies.to_h { |dep| [dep, state.results[dep]] }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Handle control flow from input transformer
|
|
277
|
+
#
|
|
278
|
+
# @param transformed [String, Hash] Transformer result
|
|
279
|
+
# @param node_name [Symbol] Node name
|
|
280
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
281
|
+
# @param node_start_time [Time] When node started
|
|
282
|
+
# @return [Hash] Control action
|
|
283
|
+
def handle_input_control_flow(transformed, node_name, node, node_start_time)
|
|
284
|
+
return { action: :continue, skip: false, content: transformed } unless transformed.is_a?(Hash)
|
|
285
|
+
|
|
286
|
+
if transformed[:halt_workflow]
|
|
287
|
+
halt_result = build_halt_result(transformed[:content], node_name, node_start_time)
|
|
288
|
+
emit_node_stop(node_name, node, halt_result, Time.now - node_start_time, false)
|
|
289
|
+
{ action: :halt, result: halt_result }
|
|
290
|
+
elsif transformed[:goto_node]
|
|
291
|
+
{ action: :goto, target: transformed[:goto_node], content: transformed[:content] }
|
|
292
|
+
elsif transformed[:skip_execution]
|
|
293
|
+
{ action: :continue, skip: true, content: transformed[:content] }
|
|
294
|
+
else
|
|
295
|
+
{ action: :continue, skip: false, content: transformed[:content] }
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Execute the node (agent-less or with mini-swarm)
|
|
300
|
+
#
|
|
301
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
302
|
+
# @param node_name [Symbol] Node name
|
|
303
|
+
# @param input [String] Input content
|
|
304
|
+
# @param skip_execution [Boolean] Whether to skip execution
|
|
305
|
+
# @return [Result] Execution result
|
|
306
|
+
def execute_node(node, node_name, input, skip_execution)
|
|
307
|
+
if skip_execution
|
|
308
|
+
build_skip_result(node_name, input)
|
|
309
|
+
elsif node.agent_less?
|
|
310
|
+
execute_agent_less_node(node, input)
|
|
311
|
+
else
|
|
312
|
+
execute_swarm_node(node, input)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Build result for skipped execution
|
|
317
|
+
#
|
|
318
|
+
# @param node_name [Symbol] Node name
|
|
319
|
+
# @param content [String] Content to include in result
|
|
320
|
+
# @return [Result] Skip result
|
|
321
|
+
def build_skip_result(node_name, content)
|
|
322
|
+
Result.new(
|
|
323
|
+
content: content,
|
|
324
|
+
agent: "skipped:#{node_name}",
|
|
325
|
+
logs: [],
|
|
326
|
+
duration: 0.0,
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Execute an agent-less (computation-only) node
|
|
331
|
+
#
|
|
332
|
+
# @param node [Workflow::NodeBuilder] Agent-less node configuration
|
|
333
|
+
# @param input [String] Input content
|
|
334
|
+
# @return [Result] Result with input passed through
|
|
335
|
+
def execute_agent_less_node(node, input)
|
|
336
|
+
Result.new(
|
|
337
|
+
content: input,
|
|
338
|
+
agent: "computation:#{node.name}",
|
|
339
|
+
logs: [],
|
|
340
|
+
duration: 0.0,
|
|
341
|
+
)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Execute node with mini-swarm
|
|
345
|
+
#
|
|
346
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
347
|
+
# @param input [String] Input content
|
|
348
|
+
# @return [Result] Execution result
|
|
349
|
+
def execute_swarm_node(node, input)
|
|
350
|
+
mini_swarm = @workflow.build_swarm_for_node(node)
|
|
351
|
+
result = mini_swarm.execute(input)
|
|
352
|
+
@workflow.cache_agent_instances(mini_swarm, node)
|
|
353
|
+
result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Process output transformer and handle control flow
|
|
357
|
+
#
|
|
358
|
+
# @param ctx [OutputTransformerContext] Grouped transformer context
|
|
359
|
+
# @return [Hash] Control action
|
|
360
|
+
def process_output_transformer(ctx)
|
|
361
|
+
output_context = NodeContext.for_output(
|
|
362
|
+
result: ctx.result,
|
|
363
|
+
all_results: ctx.state.results,
|
|
364
|
+
original_prompt: @workflow.original_prompt,
|
|
365
|
+
node_name: ctx.node_name,
|
|
366
|
+
)
|
|
367
|
+
transformed = ctx.node.transform_output(output_context)
|
|
368
|
+
|
|
369
|
+
handle_output_control_flow(transformed, ctx)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Handle control flow from output transformer
|
|
373
|
+
#
|
|
374
|
+
# @param transformed [String, Hash] Transformer result
|
|
375
|
+
# @param ctx [OutputTransformerContext] Grouped transformer context
|
|
376
|
+
# @return [Hash] Control action
|
|
377
|
+
def handle_output_control_flow(transformed, ctx)
|
|
378
|
+
return { action: :continue, content: transformed } unless transformed.is_a?(Hash)
|
|
379
|
+
|
|
380
|
+
if transformed[:halt_workflow]
|
|
381
|
+
halt_result = Result.new(
|
|
382
|
+
content: transformed[:content],
|
|
383
|
+
agent: ctx.result.agent,
|
|
384
|
+
logs: ctx.result.logs,
|
|
385
|
+
duration: ctx.result.duration,
|
|
386
|
+
)
|
|
387
|
+
emit_node_stop(ctx.node_name, ctx.node, halt_result, Time.now - ctx.node_start_time, ctx.skip_execution)
|
|
388
|
+
{ action: :halt, result: halt_result }
|
|
389
|
+
elsif transformed[:goto_node]
|
|
390
|
+
{ action: :goto, target: transformed[:goto_node], content: transformed[:content] }
|
|
391
|
+
else
|
|
392
|
+
{ action: :continue, content: transformed[:content] || transformed }
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Update result for agent-less nodes with transformed content
|
|
397
|
+
#
|
|
398
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
399
|
+
# @param node_name [Symbol] Node name
|
|
400
|
+
# @param state [ExecutionState] Current execution state
|
|
401
|
+
# @param result [Result] Original result
|
|
402
|
+
# @return [void]
|
|
403
|
+
def update_agentless_result(node, node_name, state, result)
|
|
404
|
+
return unless node.agent_less? && state.current_input != result.content
|
|
405
|
+
|
|
406
|
+
updated_result = Result.new(
|
|
407
|
+
content: state.current_input,
|
|
408
|
+
agent: result.agent,
|
|
409
|
+
logs: result.logs,
|
|
410
|
+
duration: result.duration,
|
|
411
|
+
error: result.error,
|
|
412
|
+
)
|
|
413
|
+
state.results[node_name] = updated_result
|
|
414
|
+
state.last_result = updated_result
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Build result for halted workflow
|
|
418
|
+
#
|
|
419
|
+
# @param content [String] Content to include
|
|
420
|
+
# @param node_name [Symbol] Node name
|
|
421
|
+
# @param node_start_time [Time] When node started
|
|
422
|
+
# @return [Result] Halt result
|
|
423
|
+
def build_halt_result(content, node_name, node_start_time)
|
|
424
|
+
Result.new(
|
|
425
|
+
content: content,
|
|
426
|
+
agent: "halted:#{node_name}",
|
|
427
|
+
logs: [],
|
|
428
|
+
duration: Time.now - node_start_time,
|
|
429
|
+
)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Log node execution error
|
|
433
|
+
#
|
|
434
|
+
# @param node_name [Symbol] Node name
|
|
435
|
+
# @param result [Result] Execution result with error
|
|
436
|
+
# @return [void]
|
|
437
|
+
def log_node_error(node_name, result)
|
|
438
|
+
RubyLLM.logger.error("Workflow: Node '#{node_name}' failed: #{result.error.message}")
|
|
439
|
+
RubyLLM.logger.error(" Backtrace: #{result.error.backtrace&.first(5)&.join("\n ")}")
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Find the index of a node in the execution order
|
|
443
|
+
#
|
|
444
|
+
# @param node_name [Symbol] Node name to find
|
|
445
|
+
# @return [Integer] Index in execution order
|
|
446
|
+
# @raise [ConfigurationError] If node not found
|
|
447
|
+
def find_node_index(node_name)
|
|
448
|
+
index = @workflow.execution_order.index(node_name)
|
|
449
|
+
unless index
|
|
450
|
+
raise ConfigurationError,
|
|
451
|
+
"goto_node target '#{node_name}' not found. Available nodes: #{@workflow.execution_order.join(", ")}"
|
|
452
|
+
end
|
|
453
|
+
index
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Emit node_start event
|
|
457
|
+
#
|
|
458
|
+
# @param node_name [Symbol] Name of the node
|
|
459
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
460
|
+
# @return [void]
|
|
461
|
+
def emit_node_start(node_name, node)
|
|
462
|
+
return unless LogStream.emitter
|
|
463
|
+
|
|
464
|
+
LogStream.emit(
|
|
465
|
+
type: "node_start",
|
|
466
|
+
node: node_name.to_s,
|
|
467
|
+
agent_less: node.agent_less?,
|
|
468
|
+
agents: node.agent_configs.map { |ac| ac[:agent].to_s },
|
|
469
|
+
dependencies: node.dependencies.map(&:to_s),
|
|
470
|
+
timestamp: Time.now.utc.iso8601,
|
|
471
|
+
)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Emit node_stop event
|
|
475
|
+
#
|
|
476
|
+
# @param node_name [Symbol] Name of the node
|
|
477
|
+
# @param node [Workflow::NodeBuilder] Node configuration
|
|
478
|
+
# @param result [Result] Node execution result
|
|
479
|
+
# @param duration [Float] Node execution duration in seconds
|
|
480
|
+
# @param skipped [Boolean] Whether execution was skipped
|
|
481
|
+
# @return [void]
|
|
482
|
+
def emit_node_stop(node_name, node, result, duration, skipped)
|
|
483
|
+
return unless LogStream.emitter
|
|
484
|
+
|
|
485
|
+
LogStream.emit(
|
|
486
|
+
type: "node_stop",
|
|
487
|
+
node: node_name.to_s,
|
|
488
|
+
agent_less: node.agent_less?,
|
|
489
|
+
skipped: skipped,
|
|
490
|
+
agents: node.agent_configs.map { |ac| ac[:agent].to_s },
|
|
491
|
+
duration: duration.round(3),
|
|
492
|
+
timestamp: Time.now.utc.iso8601,
|
|
493
|
+
)
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
|
-
|
|
5
|
-
#
|
|
4
|
+
class Workflow
|
|
5
|
+
# NodeBuilder provides DSL for configuring individual nodes within a workflow
|
|
6
6
|
#
|
|
7
7
|
# A node represents a stage in a multi-step workflow where a specific set
|
|
8
8
|
# of agents collaborate. Each node creates an independent swarm execution.
|
|
@@ -20,7 +20,7 @@ module SwarmSDK
|
|
|
20
20
|
#
|
|
21
21
|
# depends_on :planning
|
|
22
22
|
# end
|
|
23
|
-
class
|
|
23
|
+
class NodeBuilder
|
|
24
24
|
attr_reader :name,
|
|
25
25
|
:agent_configs,
|
|
26
26
|
:dependencies,
|
|
@@ -5,7 +5,7 @@ require "json"
|
|
|
5
5
|
require "timeout"
|
|
6
6
|
|
|
7
7
|
module SwarmSDK
|
|
8
|
-
|
|
8
|
+
class Workflow
|
|
9
9
|
# Executes bash command transformers for node input/output transformation
|
|
10
10
|
#
|
|
11
11
|
# Transformers are shell commands that receive NodeContext data on STDIN as JSON
|
|
@@ -90,7 +90,8 @@ module SwarmSDK
|
|
|
90
90
|
# echo "$CONTENT"
|
|
91
91
|
# exit 0
|
|
92
92
|
class TransformerExecutor
|
|
93
|
-
|
|
93
|
+
# Backward compatibility alias - use Defaults module for new code
|
|
94
|
+
DEFAULT_TIMEOUT = Defaults::Timeouts::TRANSFORMER_COMMAND_SECONDS
|
|
94
95
|
|
|
95
96
|
# Result object for transformer execution
|
|
96
97
|
TransformerResult = Struct.new(:success, :content, :skip_execution, :halt, :error_message, keyword_init: true) do
|