tasker-rb 0.1.1

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 +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module StepHandler
5
+ module Mixins
6
+ # Decision mixin for TAS-53 Dynamic Workflow Decision Points
7
+ #
8
+ # ## TAS-112: Composition Pattern
9
+ #
10
+ # This module follows the composition-over-inheritance pattern. Instead of
11
+ # inheriting from a specialized Decision handler class, include this mixin
12
+ # in your Base handler.
13
+ #
14
+ # ## Usage
15
+ #
16
+ # ```ruby
17
+ # class MyDecisionHandler < TaskerCore::StepHandler::Base
18
+ # include TaskerCore::StepHandler::Mixins::Decision
19
+ #
20
+ # def call(context)
21
+ # amount = context.get_task_field('amount')
22
+ #
23
+ # if amount < 1000
24
+ # decision_success(
25
+ # steps: ['auto_approve'],
26
+ # result_data: { route_type: 'auto', amount: amount }
27
+ # )
28
+ # else
29
+ # decision_success(
30
+ # steps: ['manager_approval', 'finance_review'],
31
+ # result_data: { route_type: 'dual', amount: amount }
32
+ # )
33
+ # end
34
+ # end
35
+ # end
36
+ # ```
37
+ #
38
+ # ## No-Branch Pattern
39
+ #
40
+ # ```ruby
41
+ # def call(context)
42
+ # if context.get_task_field('skip_approval')
43
+ # decision_no_branches(result_data: { reason: 'skipped' })
44
+ # else
45
+ # decision_success(steps: ['standard_approval'])
46
+ # end
47
+ # end
48
+ # ```
49
+ module Decision
50
+ # Hook called when module is included
51
+ def self.included(base)
52
+ base.extend(ClassMethods)
53
+ end
54
+
55
+ # Class methods added to including class
56
+ module ClassMethods
57
+ # No class methods needed for now
58
+ end
59
+
60
+ # Override capabilities to include decision-specific features
61
+ def capabilities
62
+ super + %w[decision_point dynamic_workflow step_creation]
63
+ end
64
+
65
+ # Enhanced configuration schema for decision handlers
66
+ def config_schema
67
+ super.merge({
68
+ properties: super[:properties].merge(
69
+ decision_thresholds: {
70
+ type: 'object',
71
+ description: 'Thresholds for decision routing logic'
72
+ },
73
+ decision_metadata: {
74
+ type: 'object',
75
+ description: 'Additional metadata for decision logging'
76
+ }
77
+ )
78
+ })
79
+ end
80
+
81
+ # ========================================================================
82
+ # DECISION OUTCOME HELPER METHODS
83
+ # ========================================================================
84
+
85
+ # Return a successful decision outcome that creates specified steps
86
+ #
87
+ # @param steps [Array<String>, String] Step name(s) to create dynamically
88
+ # @param result_data [Hash] Additional result data (route_type, amounts, etc.)
89
+ # @param metadata [Hash] Optional metadata for observability
90
+ # @return [TaskerCore::Types::StepHandlerCallResult] Success result with decision outcome
91
+ #
92
+ # @example Single step
93
+ # decision_success(
94
+ # steps: 'approval_required',
95
+ # result_data: { route_type: 'standard' }
96
+ # )
97
+ #
98
+ # @example Multiple steps
99
+ # decision_success(
100
+ # steps: ['manager_approval', 'finance_review'],
101
+ # result_data: { route_type: 'dual_approval', amount: 10000 }
102
+ # )
103
+ def decision_success(steps:, result_data: {}, metadata: {})
104
+ # Normalize steps to array
105
+ step_names = Array(steps)
106
+
107
+ # Validate step names
108
+ validate_step_names!(step_names)
109
+
110
+ # Create decision outcome using type-safe factory
111
+ outcome = TaskerCore::Types::DecisionPointOutcome.create_steps(step_names)
112
+
113
+ # Build result with decision outcome embedded
114
+ result = result_data.merge(
115
+ decision_point_outcome: outcome.to_h
116
+ )
117
+
118
+ # Create success result
119
+ TaskerCore::Types::StepHandlerCallResult.success(
120
+ result: result,
121
+ metadata: build_decision_metadata(metadata, outcome)
122
+ )
123
+ end
124
+
125
+ # Return a decision outcome indicating no additional steps needed
126
+ #
127
+ # Use this when the decision point determines that workflow can proceed
128
+ # without creating any dynamic steps.
129
+ #
130
+ # @param result_data [Hash] Result data explaining why no branches needed
131
+ # @param metadata [Hash] Optional metadata for observability
132
+ # @return [TaskerCore::Types::StepHandlerCallResult] Success result with no-branches outcome
133
+ #
134
+ # @example
135
+ # decision_no_branches(
136
+ # result_data: { reason: 'amount_below_threshold', amount: 50 }
137
+ # )
138
+ def decision_no_branches(result_data: {}, metadata: {})
139
+ # Create no-branches outcome
140
+ outcome = TaskerCore::Types::DecisionPointOutcome.no_branches
141
+
142
+ # Build result with outcome embedded
143
+ result = result_data.merge(
144
+ decision_point_outcome: outcome.to_h
145
+ )
146
+
147
+ # Create success result
148
+ TaskerCore::Types::StepHandlerCallResult.success(
149
+ result: result,
150
+ metadata: build_decision_metadata(metadata, outcome)
151
+ )
152
+ end
153
+
154
+ # Alias for decision_no_branches for cross-language consistency
155
+ alias skip_branches decision_no_branches
156
+
157
+ # Validate that a decision point outcome is properly structured
158
+ #
159
+ # @param outcome [Hash, DecisionPointOutcome] Outcome to validate
160
+ # @raise [TaskerCore::PermanentError] if outcome is invalid
161
+ def validate_decision_outcome!(outcome)
162
+ # Convert to hash if it's a DecisionPointOutcome type
163
+ outcome_hash = if outcome.respond_to?(:to_h)
164
+ outcome.to_h
165
+ elsif outcome.is_a?(Hash)
166
+ outcome
167
+ else
168
+ raise_invalid_outcome!('Outcome must be Hash or DecisionPointOutcome')
169
+ end
170
+
171
+ # Validate type field exists
172
+ outcome_type = outcome_hash[:type] || outcome_hash['type'] ||
173
+ outcome_hash[:outcome_type] || outcome_hash['outcome_type']
174
+ unless %w[NoBranches CreateSteps no_branches create_steps].include?(outcome_type)
175
+ raise_invalid_outcome!("Invalid outcome_type: #{outcome_type}")
176
+ end
177
+
178
+ # Validate CreateSteps has step_names
179
+ normalized_type = outcome_type.downcase.gsub('_', '')
180
+ if normalized_type == 'createsteps'
181
+ step_names = outcome_hash[:step_names] || outcome_hash['step_names']
182
+ validate_step_names!(step_names)
183
+ end
184
+
185
+ outcome_hash
186
+ end
187
+
188
+ # Build a decision result with a custom outcome
189
+ #
190
+ # Use this for advanced scenarios where you need full control over the outcome
191
+ # structure. Most handlers should use decision_success or decision_no_branches.
192
+ #
193
+ # @param outcome [DecisionPointOutcome, Hash] The decision outcome
194
+ # @param result_data [Hash] Additional result data
195
+ # @param metadata [Hash] Optional metadata
196
+ # @return [TaskerCore::Types::StepHandlerCallResult] Success result
197
+ def decision_with_custom_outcome(outcome:, result_data: {}, metadata: {})
198
+ # Validate outcome structure
199
+ validated_outcome = validate_decision_outcome!(outcome)
200
+
201
+ # Build result
202
+ result = result_data.merge(
203
+ decision_point_outcome: validated_outcome
204
+ )
205
+
206
+ # Create success result
207
+ TaskerCore::Types::StepHandlerCallResult.success(
208
+ result: result,
209
+ metadata: build_decision_metadata(metadata, outcome)
210
+ )
211
+ end
212
+
213
+ private
214
+
215
+ # Validate step names for decision outcomes
216
+ def validate_step_names!(step_names)
217
+ unless step_names.is_a?(Array) && !step_names.empty?
218
+ raise_invalid_outcome!('step_names must be non-empty array')
219
+ end
220
+
221
+ unless step_names.all? { |name| name.is_a?(String) && !name.empty? }
222
+ raise_invalid_outcome!('All step names must be non-empty strings')
223
+ end
224
+
225
+ true
226
+ end
227
+
228
+ # Build metadata for decision outcomes
229
+ def build_decision_metadata(custom_metadata, outcome)
230
+ base_metadata = {
231
+ decision_point: true,
232
+ outcome_type: outcome.type,
233
+ branches_created: outcome.step_names.size,
234
+ processed_at: Time.now.utc.iso8601,
235
+ processed_by: handler_name
236
+ }
237
+
238
+ base_metadata.merge(custom_metadata)
239
+ end
240
+
241
+ # Raise a permanent error for invalid decision outcomes
242
+ def raise_invalid_outcome!(message)
243
+ raise TaskerCore::Errors::PermanentError.new(
244
+ "Invalid decision point outcome: #{message}",
245
+ error_code: 'INVALID_DECISION_OUTCOME',
246
+ context: { error_category: 'validation' }
247
+ )
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TAS-112: Composition Pattern - Mixin Modules
4
+ #
5
+ # This file exports all step handler mixins. Mixins follow the composition-over-inheritance
6
+ # pattern where specialized functionality is added via `include` rather than subclassing.
7
+ #
8
+ # ## Usage
9
+ #
10
+ # ```ruby
11
+ # class MyApiHandler < TaskerCore::StepHandler::Base
12
+ # include TaskerCore::StepHandler::Mixins::API
13
+ #
14
+ # def call(context)
15
+ # response = get('/users')
16
+ # success(result: response.body)
17
+ # end
18
+ # end
19
+ #
20
+ # class MyDecisionHandler < TaskerCore::StepHandler::Base
21
+ # include TaskerCore::StepHandler::Mixins::Decision
22
+ #
23
+ # def call(context)
24
+ # decision_success(steps: ['next_step'])
25
+ # end
26
+ # end
27
+ #
28
+ # class MyBatchHandler < TaskerCore::StepHandler::Base
29
+ # include TaskerCore::StepHandler::Mixins::Batchable
30
+ #
31
+ # def call(context)
32
+ # configs = create_cursor_configs(1000, 5)
33
+ # create_batches_outcome(worker_template_name: 'worker', cursor_configs: configs, total_items: 1000)
34
+ # end
35
+ # end
36
+ # ```
37
+ #
38
+ # ## Combining Mixins
39
+ #
40
+ # Mixins can be combined when a handler needs multiple capabilities:
41
+ #
42
+ # ```ruby
43
+ # class ApiBatchHandler < TaskerCore::StepHandler::Base
44
+ # include TaskerCore::StepHandler::Mixins::API
45
+ # include TaskerCore::StepHandler::Mixins::Batchable
46
+ #
47
+ # def call(context)
48
+ # # Can use both API and Batchable helpers
49
+ # end
50
+ # end
51
+ # ```
52
+
53
+ require_relative 'mixins/api'
54
+ require_relative 'mixins/decision'
55
+ require_relative 'mixins/batchable'
56
+
57
+ module TaskerCore
58
+ module StepHandler
59
+ module Mixins
60
+ # Re-export for convenient access
61
+ # API = TaskerCore::StepHandler::Mixins::API
62
+ # Decision = TaskerCore::StepHandler::Mixins::Decision
63
+ # Batchable = TaskerCore::StepHandler::Mixins::Batchable
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Worker
5
+ # Subscribes to step execution events and routes to handlers
6
+ class StepExecutionSubscriber
7
+ attr_reader :logger, :handler_registry, :stats
8
+
9
+ def initialize
10
+ @logger = TaskerCore::Logger.instance
11
+ @handler_registry = TaskerCore::Registry::HandlerRegistry.instance
12
+ @stats = { processed: 0, succeeded: 0, failed: 0 }
13
+ @active = true
14
+ @subscribed = false
15
+
16
+ subscribe_to_events
17
+ logger.info 'Step execution subscriber initialized'
18
+ end
19
+
20
+ # Check if subscriber is active
21
+ def active?
22
+ @active
23
+ end
24
+
25
+ # Stop the subscriber
26
+ def stop!
27
+ @active = false
28
+ logger.info 'Step execution subscriber stopped'
29
+ end
30
+
31
+ # Called by dry-events when step execution is requested
32
+ def call(event)
33
+ event_data = event.payload
34
+ step_data = event_data[:task_sequence_step]
35
+
36
+ logger.info 'Processing step execution request'
37
+ logger.info("Event ID: #{event_data[:event_id]}")
38
+ logger.info("Step: #{step_data.workflow_step.name}")
39
+ logger.info("Handler: #{step_data.step_definition.handler.callable}")
40
+
41
+ @stats[:processed] += 1
42
+
43
+ begin
44
+ # TAS-93: Resolve step handler from registry using full handler definition
45
+ # This enables method dispatch (handler_method) and resolver hints (resolver)
46
+ handler = @handler_registry.resolve_handler(step_data.step_definition.handler)
47
+
48
+ unless handler
49
+ raise Errors::ConfigurationError,
50
+ "No handler found for #{step_data.step_definition.handler.callable}"
51
+ end
52
+
53
+ # Execute handler with unified context (TAS-96: cross-language standard)
54
+ context = TaskerCore::Types::StepContext.new(step_data)
55
+ result = handler.call(context)
56
+
57
+ # Convert handler output to standardized result
58
+ standardized_result = TaskerCore::Types::StepHandlerCallResult.from_handler_output(result)
59
+
60
+ # TAS-125: Check for checkpoint yield - this is a special case where
61
+ # we persist progress and re-dispatch instead of completing the step
62
+ if standardized_result.checkpoint?
63
+ publish_checkpoint_yield(
64
+ event_data: event_data,
65
+ checkpoint_result: standardized_result
66
+ )
67
+ @stats[:succeeded] += 1
68
+ logger.info('āœ… Checkpoint yield submitted - step will be re-dispatched')
69
+ return
70
+ end
71
+
72
+ # Handle handler-returned error results directly (preserves retryable field)
73
+ # TAS-125: Handlers like inject_failure return StepHandlerCallResult.error()
74
+ # with explicit retryable: false for permanent failures. We must preserve this.
75
+ unless standardized_result.success?
76
+ logger.error("šŸ’„ Handler returned failure: #{standardized_result.message}")
77
+ logger.error("šŸ’„ Error type: #{standardized_result.error_type}, retryable: #{standardized_result.retryable}")
78
+
79
+ publish_step_completion(
80
+ event_data: event_data,
81
+ success: false,
82
+ result: nil,
83
+ error_message: standardized_result.message,
84
+ metadata: {
85
+ execution_time_ms: standardized_result.metadata&.dig('duration_ms') || 0,
86
+ handler_version: nil,
87
+ retryable: standardized_result.retryable, # CRITICAL: Preserve handler's retryable setting
88
+ completed_at: Time.now.utc.iso8601,
89
+ worker_id: 'ruby_worker',
90
+ worker_hostname: nil,
91
+ started_at: Time.now.utc.iso8601,
92
+ custom: {
93
+ failed_at: Time.now.utc.iso8601,
94
+ failed_by: 'ruby_worker',
95
+ error_type: standardized_result.error_type,
96
+ error_code: standardized_result.error_code,
97
+ handler_class: step_data.step_definition.handler.callable,
98
+ retryable: standardized_result.retryable
99
+ }.merge(standardized_result.metadata || {})
100
+ }
101
+ )
102
+
103
+ @stats[:failed] += 1
104
+ return
105
+ end
106
+
107
+ # Publish successful completion with properly structured metadata
108
+ # Match StepExecutionMetadata struct from Rust
109
+ publish_step_completion(
110
+ event_data: event_data,
111
+ success: true,
112
+ result: standardized_result.result,
113
+ metadata: {
114
+ # StepExecutionMetadata required fields
115
+ execution_time_ms: standardized_result.metadata&.dig('duration_ms') || 0,
116
+ handler_version: nil,
117
+ retryable: true, # Success is always retryable if it fails later
118
+ completed_at: Time.now.utc.iso8601,
119
+ worker_id: 'ruby_worker',
120
+ worker_hostname: nil,
121
+ started_at: Time.now.utc.iso8601,
122
+ # Additional metadata in custom field
123
+ custom: {
124
+ processed_at: Time.now.utc.iso8601,
125
+ processed_by: 'ruby_worker',
126
+ handler_class: step_data.step_definition.handler.callable
127
+ }.merge(standardized_result.metadata || {})
128
+ }
129
+ )
130
+
131
+ @stats[:succeeded] += 1
132
+ logger.info('āœ… Step execution completed successfully')
133
+ rescue StandardError => e
134
+ logger.error("šŸ’„ Step execution failed: #{e.message}")
135
+ logger.error("šŸ’„ #{e.backtrace.first(5).join("\nšŸ’„ ")}")
136
+
137
+ # Classify error retryability using systematic classifier
138
+ retryable = Errors::ErrorClassifier.retryable?(e)
139
+
140
+ # Publish failure completion with properly structured metadata
141
+ # Match StepExecutionMetadata struct from Rust - retryable field is critical
142
+ publish_step_completion(
143
+ event_data: event_data,
144
+ success: false,
145
+ result: nil,
146
+ error_message: e.message,
147
+ metadata: {
148
+ # StepExecutionMetadata required fields
149
+ execution_time_ms: 0,
150
+ handler_version: nil,
151
+ retryable: retryable, # CRITICAL: This is what Rust reads at metadata.retryable
152
+ completed_at: Time.now.utc.iso8601,
153
+ worker_id: 'ruby_worker',
154
+ worker_hostname: nil,
155
+ started_at: Time.now.utc.iso8601,
156
+ # Additional error context in custom field
157
+ custom: {
158
+ failed_at: Time.now.utc.iso8601,
159
+ failed_by: 'ruby_worker',
160
+ error_class: e.class.name,
161
+ handler_class: step_data.step_definition.handler.callable,
162
+ retryable: retryable # Also in custom for debugging/redundancy
163
+ }
164
+ }
165
+ )
166
+
167
+ @stats[:failed] += 1
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def subscribe_to_events
174
+ return if @subscribed # Guard against double subscription
175
+
176
+ TaskerCore::Worker::EventBridge.instance.subscribe_to_step_execution do |event|
177
+ call(event)
178
+ end
179
+ @subscribed = true
180
+ logger.info 'Subscribed to step execution events'
181
+ end
182
+
183
+ def publish_step_completion(event_data:, success:, result: nil, error_message: nil, metadata: nil)
184
+ completion_payload = {
185
+ event_id: event_data[:event_id],
186
+ task_uuid: event_data[:task_uuid],
187
+ step_uuid: event_data[:step_uuid],
188
+ success: success,
189
+ result: result,
190
+ metadata: metadata,
191
+ error_message: error_message
192
+ }
193
+
194
+ # TAS-65 Phase 1.5b: Propagate trace context back to Rust for distributed tracing
195
+ completion_payload[:trace_id] = event_data[:trace_id] if event_data[:trace_id]
196
+ completion_payload[:span_id] = event_data[:span_id] if event_data[:span_id]
197
+
198
+ TaskerCore::Worker::EventBridge.instance.publish_step_completion(completion_payload)
199
+ end
200
+
201
+ # TAS-125: Publish checkpoint yield to persist progress and trigger re-dispatch
202
+ def publish_checkpoint_yield(event_data:, checkpoint_result:)
203
+ checkpoint_data = checkpoint_result.to_checkpoint_data(
204
+ event_id: event_data[:event_id],
205
+ step_uuid: event_data[:step_uuid]
206
+ )
207
+
208
+ TaskerCore::Worker::EventBridge.instance.publish_step_checkpoint_yield(checkpoint_data)
209
+ end
210
+ end
211
+ end
212
+ end