conductor_ruby 0.1.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 (143) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/LICENSE +190 -0
  4. data/README.md +517 -0
  5. data/examples/agentic_workflows/llm_chat.rb +106 -0
  6. data/examples/dynamic_workflow.rb +177 -0
  7. data/examples/event_handler.rb +94 -0
  8. data/examples/event_listener_examples.rb +430 -0
  9. data/examples/helloworld/greetings_worker.rb +24 -0
  10. data/examples/helloworld/helloworld.rb +99 -0
  11. data/examples/kitchensink.rb +213 -0
  12. data/examples/metadata_journey.rb +189 -0
  13. data/examples/metrics_example.rb +284 -0
  14. data/examples/new_dsl_demo.rb +141 -0
  15. data/examples/orkes/http_poll.rb +83 -0
  16. data/examples/orkes/secrets_example.rb +69 -0
  17. data/examples/orkes/wait_for_webhook.rb +90 -0
  18. data/examples/prompt_journey.rb +245 -0
  19. data/examples/rag_workflow.rb +167 -0
  20. data/examples/schedule_journey.rb +244 -0
  21. data/examples/simple_worker.rb +125 -0
  22. data/examples/simple_workflow.rb +89 -0
  23. data/examples/task_context_example.rb +257 -0
  24. data/examples/task_listener_example.rb +192 -0
  25. data/examples/worker_configuration_example.rb +282 -0
  26. data/examples/workflow_dsl.rb +316 -0
  27. data/examples/workflow_ops.rb +305 -0
  28. data/lib/conductor/client/authorization_client.rb +238 -0
  29. data/lib/conductor/client/integration_client.rb +108 -0
  30. data/lib/conductor/client/metadata_client.rb +139 -0
  31. data/lib/conductor/client/prompt_client.rb +58 -0
  32. data/lib/conductor/client/scheduler_client.rb +132 -0
  33. data/lib/conductor/client/schema_client.rb +32 -0
  34. data/lib/conductor/client/secret_client.rb +48 -0
  35. data/lib/conductor/client/task_client.rb +168 -0
  36. data/lib/conductor/client/workflow_client.rb +242 -0
  37. data/lib/conductor/configuration/authentication_settings.rb +17 -0
  38. data/lib/conductor/configuration.rb +103 -0
  39. data/lib/conductor/exceptions.rb +86 -0
  40. data/lib/conductor/http/api/application_resource_api.rb +107 -0
  41. data/lib/conductor/http/api/authorization_resource_api.rb +56 -0
  42. data/lib/conductor/http/api/event_resource_api.rb +133 -0
  43. data/lib/conductor/http/api/gateway_auth_resource_api.rb +48 -0
  44. data/lib/conductor/http/api/group_resource_api.rb +76 -0
  45. data/lib/conductor/http/api/integration_resource_api.rb +145 -0
  46. data/lib/conductor/http/api/metadata_resource_api.rb +231 -0
  47. data/lib/conductor/http/api/prompt_resource_api.rb +81 -0
  48. data/lib/conductor/http/api/role_resource_api.rb +60 -0
  49. data/lib/conductor/http/api/scheduler_resource_api.rb +211 -0
  50. data/lib/conductor/http/api/schema_resource_api.rb +82 -0
  51. data/lib/conductor/http/api/secret_resource_api.rb +134 -0
  52. data/lib/conductor/http/api/task_resource_api.rb +321 -0
  53. data/lib/conductor/http/api/token_resource_api.rb +42 -0
  54. data/lib/conductor/http/api/user_resource_api.rb +59 -0
  55. data/lib/conductor/http/api/workflow_bulk_resource_api.rb +91 -0
  56. data/lib/conductor/http/api/workflow_resource_api.rb +451 -0
  57. data/lib/conductor/http/api_client.rb +437 -0
  58. data/lib/conductor/http/models/authentication_config.rb +67 -0
  59. data/lib/conductor/http/models/authorization_request.rb +39 -0
  60. data/lib/conductor/http/models/base_model.rb +162 -0
  61. data/lib/conductor/http/models/bulk_response.rb +39 -0
  62. data/lib/conductor/http/models/conductor_application.rb +39 -0
  63. data/lib/conductor/http/models/conductor_user.rb +53 -0
  64. data/lib/conductor/http/models/create_or_update_application_request.rb +24 -0
  65. data/lib/conductor/http/models/create_or_update_role_request.rb +27 -0
  66. data/lib/conductor/http/models/event_handler.rb +130 -0
  67. data/lib/conductor/http/models/generate_token_request.rb +27 -0
  68. data/lib/conductor/http/models/group.rb +36 -0
  69. data/lib/conductor/http/models/integration.rb +70 -0
  70. data/lib/conductor/http/models/integration_api.rb +53 -0
  71. data/lib/conductor/http/models/integration_api_update.rb +43 -0
  72. data/lib/conductor/http/models/integration_update.rb +36 -0
  73. data/lib/conductor/http/models/permission.rb +24 -0
  74. data/lib/conductor/http/models/poll_data.rb +33 -0
  75. data/lib/conductor/http/models/prompt_template.rb +59 -0
  76. data/lib/conductor/http/models/prompt_template_test_request.rb +43 -0
  77. data/lib/conductor/http/models/rerun_workflow_request.rb +37 -0
  78. data/lib/conductor/http/models/role.rb +27 -0
  79. data/lib/conductor/http/models/schema_def.rb +59 -0
  80. data/lib/conductor/http/models/search_result.rb +187 -0
  81. data/lib/conductor/http/models/skip_task_request.rb +27 -0
  82. data/lib/conductor/http/models/start_workflow_request.rb +68 -0
  83. data/lib/conductor/http/models/subject_ref.rb +35 -0
  84. data/lib/conductor/http/models/tag_object.rb +36 -0
  85. data/lib/conductor/http/models/target_ref.rb +39 -0
  86. data/lib/conductor/http/models/task.rb +156 -0
  87. data/lib/conductor/http/models/task_def.rb +95 -0
  88. data/lib/conductor/http/models/task_exec_log.rb +30 -0
  89. data/lib/conductor/http/models/task_result.rb +115 -0
  90. data/lib/conductor/http/models/task_result_status.rb +24 -0
  91. data/lib/conductor/http/models/token.rb +33 -0
  92. data/lib/conductor/http/models/upsert_group_request.rb +30 -0
  93. data/lib/conductor/http/models/upsert_user_request.rb +39 -0
  94. data/lib/conductor/http/models/workflow.rb +202 -0
  95. data/lib/conductor/http/models/workflow_def.rb +73 -0
  96. data/lib/conductor/http/models/workflow_schedule.rb +100 -0
  97. data/lib/conductor/http/models/workflow_state_update.rb +30 -0
  98. data/lib/conductor/http/models/workflow_status_constants.rb +57 -0
  99. data/lib/conductor/http/models/workflow_task.rb +169 -0
  100. data/lib/conductor/http/models/workflow_test_request.rb +67 -0
  101. data/lib/conductor/http/rest_client.rb +211 -0
  102. data/lib/conductor/orkes/models/access_key.rb +56 -0
  103. data/lib/conductor/orkes/models/granted_permission.rb +27 -0
  104. data/lib/conductor/orkes/models/metadata_tag.rb +15 -0
  105. data/lib/conductor/orkes/models/rate_limit_tag.rb +15 -0
  106. data/lib/conductor/orkes/orkes_clients.rb +69 -0
  107. data/lib/conductor/version.rb +5 -0
  108. data/lib/conductor/worker/events/conductor_event.rb +40 -0
  109. data/lib/conductor/worker/events/global_dispatcher.rb +37 -0
  110. data/lib/conductor/worker/events/http_events.rb +25 -0
  111. data/lib/conductor/worker/events/listener_registry.rb +40 -0
  112. data/lib/conductor/worker/events/listeners.rb +34 -0
  113. data/lib/conductor/worker/events/sync_event_dispatcher.rb +78 -0
  114. data/lib/conductor/worker/events/task_runner_events.rb +271 -0
  115. data/lib/conductor/worker/events/workflow_events.rb +49 -0
  116. data/lib/conductor/worker/fiber_executor.rb +532 -0
  117. data/lib/conductor/worker/ractor_task_runner.rb +501 -0
  118. data/lib/conductor/worker/task_context.rb +114 -0
  119. data/lib/conductor/worker/task_definition_registrar.rb +322 -0
  120. data/lib/conductor/worker/task_handler.rb +360 -0
  121. data/lib/conductor/worker/task_in_progress.rb +60 -0
  122. data/lib/conductor/worker/task_runner.rb +538 -0
  123. data/lib/conductor/worker/telemetry/metrics_collector.rb +196 -0
  124. data/lib/conductor/worker/telemetry/prometheus_backend.rb +224 -0
  125. data/lib/conductor/worker/worker.rb +355 -0
  126. data/lib/conductor/worker/worker_config.rb +154 -0
  127. data/lib/conductor/worker/worker_registry.rb +71 -0
  128. data/lib/conductor/workflow/dsl/input_ref.rb +37 -0
  129. data/lib/conductor/workflow/dsl/output_ref.rb +44 -0
  130. data/lib/conductor/workflow/dsl/parallel_builder.rb +49 -0
  131. data/lib/conductor/workflow/dsl/switch_builder.rb +74 -0
  132. data/lib/conductor/workflow/dsl/task_ref.rb +178 -0
  133. data/lib/conductor/workflow/dsl/workflow_builder.rb +1016 -0
  134. data/lib/conductor/workflow/dsl/workflow_definition.rb +150 -0
  135. data/lib/conductor/workflow/llm/chat_message.rb +47 -0
  136. data/lib/conductor/workflow/llm/embedding_model.rb +19 -0
  137. data/lib/conductor/workflow/llm/tool_call.rb +43 -0
  138. data/lib/conductor/workflow/llm/tool_spec.rb +46 -0
  139. data/lib/conductor/workflow/task_type.rb +68 -0
  140. data/lib/conductor/workflow/timeout_policy.rb +31 -0
  141. data/lib/conductor/workflow/workflow_executor.rb +373 -0
  142. data/lib/conductor.rb +192 -0
  143. metadata +359 -0
@@ -0,0 +1,355 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../http/models/task'
4
+ require_relative '../http/models/task_result'
5
+ require_relative '../http/models/task_result_status'
6
+ require_relative '../exceptions'
7
+ require_relative 'worker_registry'
8
+ require_relative 'task_in_progress'
9
+
10
+ module Conductor
11
+ module Worker
12
+ # Worker class that wraps an execute function
13
+ # Handles various return types and keyword argument mapping
14
+ class Worker
15
+ # @return [String] Task definition name in Conductor
16
+ attr_reader :task_definition_name
17
+
18
+ # @return [Proc, Method, nil] Function to execute tasks
19
+ attr_reader :execute_function
20
+
21
+ # Configuration attributes
22
+ attr_accessor :poll_interval, :thread_count, :domain, :worker_id,
23
+ :poll_timeout, :register_task_def, :overwrite_task_def,
24
+ :strict_schema, :paused, :isolation, :executor,
25
+ :task_def_template
26
+
27
+ # Default configuration values
28
+ DEFAULTS = {
29
+ poll_interval: 100, # milliseconds
30
+ thread_count: 1,
31
+ domain: nil,
32
+ worker_id: nil,
33
+ poll_timeout: 100, # milliseconds
34
+ register_task_def: false,
35
+ overwrite_task_def: true,
36
+ strict_schema: false,
37
+ paused: false,
38
+ isolation: :thread,
39
+ executor: :thread_pool
40
+ }.freeze
41
+
42
+ # Initialize a worker
43
+ # @param task_definition_name [String] Task definition name in Conductor
44
+ # @param execute_function [Proc, Method, nil] Function to execute tasks
45
+ # @param options [Hash] Worker configuration options
46
+ # @yield [task] Block to execute tasks (alternative to execute_function)
47
+ def initialize(task_definition_name, execute_function = nil, **options, &block)
48
+ @task_definition_name = task_definition_name
49
+ @execute_function = execute_function || block
50
+
51
+ raise ArgumentError, 'execute_function or block required' unless @execute_function
52
+
53
+ # Apply options with defaults
54
+ DEFAULTS.each do |key, default|
55
+ value = options.key?(key) ? options[key] : default
56
+ send("#{key}=", value)
57
+ end
58
+
59
+ @task_def_template = options[:task_def_template]
60
+
61
+ # Analyze the execute function for parameter mapping
62
+ @takes_task_object = analyze_execute_function
63
+ end
64
+
65
+ # Alias for task_definition_name (compatibility)
66
+ def task_type
67
+ @task_definition_name
68
+ end
69
+
70
+ # Get polling interval in seconds
71
+ # @return [Float]
72
+ def polling_interval_seconds
73
+ @poll_interval / 1000.0
74
+ end
75
+
76
+ # Execute a task
77
+ # Handles keyword argument mapping and various return types
78
+ # @param task [Task] The task to execute
79
+ # @return [TaskResult, TaskInProgress] Execution result
80
+ def execute(task)
81
+ # Convert task if needed
82
+ task_obj = task.is_a?(Http::Models::Task) ? task : Http::Models::Task.from_hash(task)
83
+
84
+ # Call the execute function with appropriate arguments
85
+ output = call_execute_function(task_obj)
86
+
87
+ # Handle different return types
88
+ convert_output_to_result(output, task_obj)
89
+ end
90
+
91
+ # Define a worker using a block
92
+ # Registers the worker in the global registry
93
+ # @param task_definition_name [String] Task definition name
94
+ # @param options [Hash] Worker configuration options
95
+ # @yield [task] Block to execute tasks
96
+ # @return [Worker] The created worker
97
+ def self.define(task_definition_name, **options, &block)
98
+ worker = new(task_definition_name, nil, **options, &block)
99
+ WorkerRegistry.register(task_definition_name, block, options)
100
+ worker
101
+ end
102
+
103
+ private
104
+
105
+ # Analyze the execute function to determine how to call it
106
+ # @return [Boolean] True if function takes a Task object directly
107
+ def analyze_execute_function
108
+ return true unless @execute_function.respond_to?(:parameters)
109
+
110
+ params = @execute_function.parameters
111
+ return true if params.empty?
112
+
113
+ # Check if first parameter is a positional arg (likely task)
114
+ first_param_type, first_param_name = params.first
115
+
116
+ # If it's a positional arg (required or optional), assume it takes task
117
+ return true if %i[req opt rest].include?(first_param_type)
118
+
119
+ # If it's a keyword arg named 'task', it takes task
120
+ return true if first_param_name == :task && %i[keyreq key].include?(first_param_type)
121
+
122
+ # Otherwise, it uses keyword args from input_data
123
+ false
124
+ end
125
+
126
+ # Call the execute function with appropriate arguments
127
+ # @param task [Task] The task object
128
+ # @return [Object] Raw output from the execute function
129
+ def call_execute_function(task)
130
+ if @takes_task_object
131
+ # Pass the full task object
132
+ @execute_function.call(task)
133
+ else
134
+ # Map input_data to keyword arguments
135
+ kwargs = extract_keyword_args(task)
136
+ @execute_function.call(**kwargs)
137
+ end
138
+ end
139
+
140
+ # Extract keyword arguments from task input_data
141
+ # @param task [Task] The task object
142
+ # @return [Hash] Keyword arguments
143
+ def extract_keyword_args(task)
144
+ input_data = task.input_data || {}
145
+ kwargs = {}
146
+
147
+ return kwargs unless @execute_function.respond_to?(:parameters)
148
+
149
+ @execute_function.parameters.each do |type, name|
150
+ key = name.to_s
151
+ sym_key = name.to_sym
152
+
153
+ case type
154
+ when :keyreq # Required keyword arg
155
+ kwargs[sym_key] = if input_data.key?(key)
156
+ input_data[key]
157
+ elsif input_data.key?(sym_key)
158
+ input_data[sym_key]
159
+ end
160
+ when :key # Optional keyword arg
161
+ if input_data.key?(key)
162
+ kwargs[sym_key] = input_data[key]
163
+ elsif input_data.key?(sym_key)
164
+ kwargs[sym_key] = input_data[sym_key]
165
+ end
166
+ # Don't include if not in input_data (use default)
167
+ when :keyrest # **kwargs
168
+ # Include all remaining input_data
169
+ input_data.each do |k, v|
170
+ kwargs[k.to_sym] = v unless kwargs.key?(k.to_sym)
171
+ end
172
+ end
173
+ end
174
+
175
+ kwargs
176
+ end
177
+
178
+ # Convert execute function output to TaskResult
179
+ # @param output [Object] Raw output from execute function
180
+ # @param task [Task] The task object
181
+ # @return [TaskResult, TaskInProgress]
182
+ def convert_output_to_result(output, task)
183
+ task_result = case output
184
+ when Http::Models::TaskResult
185
+ output
186
+ when TaskInProgress
187
+ result = Http::Models::TaskResult.in_progress
188
+ result.callback_after_seconds = output.callback_after_seconds
189
+ result.output_data = output.output if output.output
190
+ result
191
+ when Hash
192
+ result = Http::Models::TaskResult.complete
193
+ result.output_data = output
194
+ result
195
+ when true
196
+ Http::Models::TaskResult.complete
197
+ when false
198
+ Http::Models::TaskResult.failed('Worker returned false')
199
+ when nil
200
+ Http::Models::TaskResult.complete
201
+ else
202
+ result = Http::Models::TaskResult.complete
203
+ result.output_data = { 'result' => output }
204
+ result
205
+ end
206
+
207
+ # Set task identifiers
208
+ task_result.task_id = task.task_id
209
+ task_result.workflow_instance_id = task.workflow_instance_id
210
+
211
+ task_result
212
+ end
213
+ end
214
+
215
+ # Mixin module for class-based workers
216
+ # Include this in your worker class to use the worker_task DSL
217
+ #
218
+ # @example
219
+ # class MyWorker
220
+ # include Conductor::Worker::WorkerMixin
221
+ #
222
+ # worker_task 'my_task', poll_interval: 200, thread_count: 5
223
+ #
224
+ # def execute(task)
225
+ # { result: 'success' }
226
+ # end
227
+ # end
228
+ module WorkerMixin
229
+ def self.included(base)
230
+ base.extend(ClassMethods)
231
+ base.class_eval do
232
+ attr_accessor :poll_interval, :thread_count, :domain, :worker_id,
233
+ :poll_timeout, :register_task_def, :overwrite_task_def,
234
+ :strict_schema, :paused, :isolation, :executor
235
+ end
236
+ end
237
+
238
+ module ClassMethods
239
+ # Define a worker for a specific task type
240
+ # @param task_definition_name [String] Task definition name
241
+ # @param options [Hash] Worker options
242
+ def worker_task(task_definition_name, **options)
243
+ @task_definition_name = task_definition_name
244
+ @worker_options = options
245
+
246
+ # Apply defaults
247
+ Worker::DEFAULTS.each do |key, default|
248
+ instance_variable_set("@#{key}", options.fetch(key, default))
249
+ end
250
+ end
251
+
252
+ # @return [String] Task definition name
253
+ def task_definition_name
254
+ @task_definition_name
255
+ end
256
+
257
+ # Alias for compatibility
258
+ def task_type
259
+ @task_definition_name
260
+ end
261
+
262
+ # @return [Hash] Worker options
263
+ def worker_options
264
+ @worker_options || {}
265
+ end
266
+
267
+ # Configuration readers
268
+ Worker::DEFAULTS.each_key do |key|
269
+ define_method(key) do
270
+ instance_variable_get("@#{key}") || Worker::DEFAULTS[key]
271
+ end
272
+ end
273
+ end
274
+
275
+ # Execute the task - must be overridden by worker class
276
+ # @param task [Task] The task to execute
277
+ # @return [TaskResult, Hash, Boolean, nil] Execution result
278
+ def execute(task)
279
+ raise NotImplementedError, 'Worker must implement #execute method'
280
+ end
281
+
282
+ # Get polling interval in seconds
283
+ # @return [Float]
284
+ def polling_interval_seconds
285
+ (self.class.poll_interval || Worker::DEFAULTS[:poll_interval]) / 1000.0
286
+ end
287
+
288
+ # Convenience method to get task input data
289
+ # @param task [Task] The task
290
+ # @return [Hash] Input data
291
+ def get_input_data(task)
292
+ task.input_data || {}
293
+ end
294
+
295
+ # Convenience method to get a specific input value
296
+ # @param task [Task] The task
297
+ # @param key [String, Symbol] The input key
298
+ # @param default [Object] Default value if key not found
299
+ # @return [Object] The input value
300
+ def get_input(task, key, default = nil)
301
+ get_input_data(task)[key.to_s] || default
302
+ end
303
+ end
304
+
305
+ # Module for method annotation style workers
306
+ # Extend this in your module to use worker_task on methods
307
+ #
308
+ # @example
309
+ # module MyWorkers
310
+ # extend Conductor::Worker::Annotatable
311
+ #
312
+ # worker_task 'greet_user', poll_interval: 100
313
+ # def self.greet(name:, greeting: 'Hello')
314
+ # "#{greeting}, #{name}!"
315
+ # end
316
+ # end
317
+ module Annotatable
318
+ # Mark the next defined method as a worker
319
+ # @param task_definition_name [String] Task definition name
320
+ # @param options [Hash] Worker options
321
+ def worker_task(task_definition_name, **options)
322
+ @pending_worker_task = {
323
+ task_definition_name: task_definition_name,
324
+ options: options
325
+ }
326
+ end
327
+
328
+ # Hook called when a method is added
329
+ def singleton_method_added(method_name)
330
+ super
331
+ return unless @pending_worker_task
332
+
333
+ pending = @pending_worker_task
334
+ @pending_worker_task = nil
335
+
336
+ # Get the method and register it
337
+ method_obj = method(method_name)
338
+ WorkerRegistry.register(
339
+ pending[:task_definition_name],
340
+ method_obj,
341
+ pending[:options]
342
+ )
343
+ end
344
+ end
345
+
346
+ # Module-level worker_task for defining workers at the top level
347
+ # @param task_definition_name [String] Task definition name
348
+ # @param options [Hash] Worker options
349
+ # @yield [task] Block to execute tasks
350
+ # @return [Worker] The created worker
351
+ def self.worker_task(task_definition_name, **options, &block)
352
+ Worker.define(task_definition_name, **options, &block)
353
+ end
354
+ end
355
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ module Conductor
6
+ module Worker
7
+ # Configuration resolver for workers with 3-tier hierarchy
8
+ # Priority (highest to lowest):
9
+ # 1. Worker-specific env var: conductor.worker.{task_name}.{property}
10
+ # 2. Global worker env var: conductor.worker.all.{property}
11
+ # 3. Code-level default from worker definition
12
+ class WorkerConfig
13
+ # Configuration properties with their types and default values
14
+ PROPERTIES = {
15
+ poll_interval: { type: :integer, default: 100 }, # milliseconds
16
+ thread_count: { type: :integer, default: 1 },
17
+ domain: { type: :string, default: nil },
18
+ worker_id: { type: :string, default: nil }, # auto-generated if nil
19
+ poll_timeout: { type: :integer, default: 100 }, # milliseconds
20
+ register_task_def: { type: :boolean, default: false },
21
+ overwrite_task_def: { type: :boolean, default: true },
22
+ strict_schema: { type: :boolean, default: false },
23
+ paused: { type: :boolean, default: false },
24
+ isolation: { type: :symbol, default: :thread }, # :thread or :ractor
25
+ executor: { type: :symbol, default: :thread_pool } # :thread_pool or :fiber
26
+ }.freeze
27
+
28
+ class << self
29
+ # Resolve configuration for a worker
30
+ # @param worker_name [String] Task definition name
31
+ # @param defaults [Hash] Code-level defaults from worker definition
32
+ # @return [Hash] Resolved configuration
33
+ def resolve(worker_name, defaults = {})
34
+ result = {}
35
+
36
+ PROPERTIES.each do |property, config|
37
+ # Try to get from environment variables (3-tier hierarchy)
38
+ env_value = get_env_value(worker_name, property)
39
+
40
+ result[property] = if env_value
41
+ convert_value(env_value, config[:type])
42
+ elsif defaults.key?(property)
43
+ defaults[property]
44
+ else
45
+ config[:default]
46
+ end
47
+ end
48
+
49
+ # Auto-generate worker_id if not set
50
+ result[:worker_id] ||= generate_worker_id
51
+
52
+ result
53
+ end
54
+
55
+ # Generate a unique worker ID
56
+ # @return [String]
57
+ def generate_worker_id
58
+ hostname = begin
59
+ Socket.gethostname
60
+ rescue StandardError
61
+ 'unknown'
62
+ end
63
+ pid = Process.pid
64
+ thread_id = Thread.current.object_id.to_s(16)
65
+ "#{hostname}-#{pid}-#{thread_id}"
66
+ end
67
+
68
+ private
69
+
70
+ # Get a value from environment variables using the 3-tier hierarchy
71
+ # @param worker_name [String] Task definition name
72
+ # @param property [Symbol] Property name
73
+ # @return [String, nil] Value from environment or nil
74
+ def get_env_value(worker_name, property)
75
+ property_str = property.to_s
76
+ worker_name_normalized = normalize_worker_name(worker_name)
77
+
78
+ # Priority 1: Worker-specific env vars
79
+ # conductor.worker.{task_name}.{property} (dotted format)
80
+ value = ENV.fetch("conductor.worker.#{worker_name}.#{property_str}", nil)
81
+ return value if value
82
+
83
+ # CONDUCTOR_WORKER_{TASK_NAME}_{PROPERTY} (uppercase format)
84
+ value = ENV.fetch("CONDUCTOR_WORKER_#{worker_name_normalized}_#{property_str.upcase}", nil)
85
+ return value if value
86
+
87
+ # Priority 2: Global worker env vars
88
+ # conductor.worker.all.{property} (dotted format)
89
+ value = ENV.fetch("conductor.worker.all.#{property_str}", nil)
90
+ return value if value
91
+
92
+ # CONDUCTOR_WORKER_ALL_{PROPERTY} (uppercase format)
93
+ value = ENV.fetch("CONDUCTOR_WORKER_ALL_#{property_str.upcase}", nil)
94
+ return value if value
95
+
96
+ # Priority 3: Legacy format
97
+ # CONDUCTOR_WORKER_{PROPERTY} (old global format)
98
+ value = ENV.fetch("CONDUCTOR_WORKER_#{property_str.upcase}", nil)
99
+ return value if value
100
+
101
+ # Special backward compatibility for poll_interval
102
+ if property == :poll_interval
103
+ value = ENV.fetch('POLLING_INTERVAL', nil)
104
+ return value if value
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ # Normalize worker name for environment variable lookup
111
+ # Converts task names like "my-task" to "MY_TASK"
112
+ # @param name [String] Worker name
113
+ # @return [String] Normalized name
114
+ def normalize_worker_name(name)
115
+ name.to_s.gsub(/[^a-zA-Z0-9]/, '_').upcase
116
+ end
117
+
118
+ # Convert a string value to the appropriate type
119
+ # @param value [String] String value from environment
120
+ # @param type [Symbol] Target type (:integer, :boolean, :string, :symbol)
121
+ # @return [Object] Converted value
122
+ def convert_value(value, type)
123
+ case type
124
+ when :integer
125
+ value.to_i
126
+ when :boolean
127
+ parse_boolean(value)
128
+ when :symbol
129
+ value.to_sym
130
+ when :string
131
+ value
132
+ else
133
+ value
134
+ end
135
+ end
136
+
137
+ # Parse a boolean value from string
138
+ # Accepts: true/1/yes and false/0/no (case-insensitive)
139
+ # @param value [String] String value
140
+ # @return [Boolean]
141
+ def parse_boolean(value)
142
+ case value.to_s.downcase
143
+ when 'true', '1', 'yes'
144
+ true
145
+ when 'false', '0', 'no'
146
+ false
147
+ else
148
+ false
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ module Worker
5
+ # Global registry for workers defined via worker_task DSL
6
+ # Workers are registered when their defining code is loaded
7
+ class WorkerRegistry
8
+ class << self
9
+ # Register a worker definition
10
+ # @param task_definition_name [String] Task definition name
11
+ # @param execute_function [Proc, Method] Function to execute tasks
12
+ # @param options [Hash] Worker configuration options
13
+ # @return [void]
14
+ def register(task_definition_name, execute_function, options = {})
15
+ key = [task_definition_name, options[:domain]]
16
+ registry[key] = {
17
+ task_definition_name: task_definition_name,
18
+ execute_function: execute_function,
19
+ options: options
20
+ }
21
+ end
22
+
23
+ # Get all registered worker definitions
24
+ # @return [Array<Hash>] Array of worker definition hashes
25
+ def all
26
+ registry.values
27
+ end
28
+
29
+ # Get a specific worker definition
30
+ # @param task_definition_name [String] Task definition name
31
+ # @param domain [String, nil] Optional domain
32
+ # @return [Hash, nil] Worker definition or nil
33
+ def get(task_definition_name, domain: nil)
34
+ registry[[task_definition_name, domain]]
35
+ end
36
+
37
+ # Check if a worker is registered
38
+ # @param task_definition_name [String] Task definition name
39
+ # @param domain [String, nil] Optional domain
40
+ # @return [Boolean]
41
+ def registered?(task_definition_name, domain: nil)
42
+ registry.key?([task_definition_name, domain])
43
+ end
44
+
45
+ # Clear all registered workers (primarily for testing)
46
+ # @return [void]
47
+ def clear
48
+ @registry = {}
49
+ end
50
+
51
+ # Get the count of registered workers
52
+ # @return [Integer]
53
+ def count
54
+ registry.size
55
+ end
56
+
57
+ # Get all registered task definition names
58
+ # @return [Array<String>]
59
+ def task_names
60
+ registry.keys.map(&:first).uniq
61
+ end
62
+
63
+ private
64
+
65
+ def registry
66
+ @registry ||= {}
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ module Workflow
5
+ module Dsl
6
+ # InputRef provides access to workflow inputs and variables
7
+ # wf[:order_id] => "${workflow.input.order_id}"
8
+ # wf.var(:counter) => "${workflow.variables.counter}"
9
+ class InputRef
10
+ # Access workflow input by field name
11
+ # @param field [String, Symbol] The input field name
12
+ # @return [OutputRef] An OutputRef pointing to the workflow input
13
+ def [](field)
14
+ OutputRef.new("workflow.input.#{field}")
15
+ end
16
+
17
+ # Access workflow variable by name
18
+ # @param name [String, Symbol] The variable name
19
+ # @return [OutputRef] An OutputRef pointing to the workflow variable
20
+ def var(name)
21
+ OutputRef.new("workflow.variables.#{name}")
22
+ end
23
+
24
+ # Access workflow output (for sub-workflows)
25
+ # @param field [String, Symbol, nil] Optional field name
26
+ # @return [OutputRef] An OutputRef pointing to workflow output
27
+ def output(field = nil)
28
+ if field
29
+ OutputRef.new("workflow.output.#{field}")
30
+ else
31
+ OutputRef.new('workflow.output')
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conductor
4
+ module Workflow
5
+ module Dsl
6
+ # OutputRef enables chained [] access for nested output paths
7
+ # task[:response][:body][:items] => "${task_ref.output.response.body.items}"
8
+ class OutputRef
9
+ attr_reader :path
10
+
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+
15
+ # Enable chained [] access
16
+ # @param field [String, Symbol] The field name
17
+ # @return [OutputRef] A new OutputRef with the extended path
18
+ def [](field)
19
+ OutputRef.new("#{@path}.#{field}")
20
+ end
21
+
22
+ # Convert to expression string for use in input parameters
23
+ # @return [String] The expression in ${...} format
24
+ def to_s
25
+ "${#{@path}}"
26
+ end
27
+
28
+ # Allow use in string interpolation
29
+ alias to_str to_s
30
+
31
+ # Compare OutputRefs by their paths
32
+ def ==(other)
33
+ other.is_a?(OutputRef) && @path == other.path
34
+ end
35
+
36
+ alias eql? ==
37
+
38
+ def hash
39
+ @path.hash
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end