smith-agents 0.4.4 → 0.5.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +262 -0
  3. data/README.md +116 -6
  4. data/docs/CONFIGURATION.md +4 -1
  5. data/docs/PATTERNS.md +7 -0
  6. data/docs/PERSISTENCE.md +502 -1
  7. data/docs/workflow_claim.md +4 -1
  8. data/lib/smith/agent/lifecycle.rb +19 -10
  9. data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
  10. data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
  11. data/lib/smith/agent/registry.rb +8 -0
  12. data/lib/smith/agent.rb +24 -2
  13. data/lib/smith/budget/amount_contract.rb +67 -0
  14. data/lib/smith/budget/amount_representation.rb +43 -0
  15. data/lib/smith/budget/decimal_context.rb +18 -0
  16. data/lib/smith/budget/ledger.rb +110 -18
  17. data/lib/smith/budget/ledger_publication.rb +60 -0
  18. data/lib/smith/budget/ledger_state.rb +21 -0
  19. data/lib/smith/budget/ledger_state_transition.rb +72 -0
  20. data/lib/smith/budget/public_state_validator.rb +25 -0
  21. data/lib/smith/budget/reservation.rb +20 -0
  22. data/lib/smith/budget/reservation_contract.rb +34 -0
  23. data/lib/smith/context/observation_masking.rb +3 -1
  24. data/lib/smith/context/state_injection.rb +1 -1
  25. data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -3
  26. data/lib/smith/error.rb +5 -0
  27. data/lib/smith/errors.rb +6 -0
  28. data/lib/smith/exponential_backoff.rb +128 -0
  29. data/lib/smith/persistence_adapters/active_record_connection_errors.rb +23 -0
  30. data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
  31. data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
  32. data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
  33. data/lib/smith/persistence_adapters/active_record_initial_write.rb +42 -0
  34. data/lib/smith/persistence_adapters/active_record_store.rb +95 -52
  35. data/lib/smith/persistence_adapters/cache_store.rb +4 -1
  36. data/lib/smith/persistence_adapters/memory.rb +45 -14
  37. data/lib/smith/persistence_adapters/payload_version.rb +23 -0
  38. data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
  39. data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
  40. data/lib/smith/persistence_adapters/redis_store.rb +38 -43
  41. data/lib/smith/persistence_adapters/redis_versioned_write.rb +56 -0
  42. data/lib/smith/persistence_adapters/retry.rb +22 -10
  43. data/lib/smith/persistence_adapters/version_expectation.rb +19 -0
  44. data/lib/smith/persistence_adapters.rb +26 -18
  45. data/lib/smith/persistence_payload_conflict.rb +12 -0
  46. data/lib/smith/tool/budget_enforcement.rb +2 -2
  47. data/lib/smith/types.rb +1 -0
  48. data/lib/smith/version.rb +2 -1
  49. data/lib/smith/workflow/branch_env.rb +1 -1
  50. data/lib/smith/workflow/budget_integration.rb +21 -11
  51. data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
  52. data/lib/smith/workflow/definition_identity.rb +25 -0
  53. data/lib/smith/workflow/deterministic_step.rb +2 -2
  54. data/lib/smith/workflow/dsl.rb +73 -5
  55. data/lib/smith/workflow/durability.rb +2 -1
  56. data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
  57. data/lib/smith/workflow/execution.rb +16 -18
  58. data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
  59. data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
  60. data/lib/smith/workflow/fanout_execution.rb +1 -1
  61. data/lib/smith/workflow/graph/diagnostic.rb +27 -1
  62. data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
  63. data/lib/smith/workflow/graph/execution_successors.rb +65 -0
  64. data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
  65. data/lib/smith/workflow/graph/metrics.rb +1 -3
  66. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
  67. data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
  68. data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
  69. data/lib/smith/workflow/graph/reachability.rb +19 -31
  70. data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
  71. data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
  72. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
  73. data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
  74. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
  75. data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
  76. data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
  77. data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
  78. data/lib/smith/workflow/graph/transition_contract.rb +47 -0
  79. data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
  80. data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
  81. data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
  82. data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
  83. data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
  84. data/lib/smith/workflow/graph/validator.rb +1 -1
  85. data/lib/smith/workflow/graph.rb +62 -4
  86. data/lib/smith/workflow/guardrail_integration.rb +23 -9
  87. data/lib/smith/workflow/identifier.rb +24 -0
  88. data/lib/smith/workflow/message_admission.rb +40 -0
  89. data/lib/smith/workflow/message_admission_boundary.rb +22 -0
  90. data/lib/smith/workflow/message_batch.rb +60 -0
  91. data/lib/smith/workflow/message_value_normalizer.rb +133 -0
  92. data/lib/smith/workflow/nested_execution.rb +10 -0
  93. data/lib/smith/workflow/orchestrator_worker.rb +3 -7
  94. data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
  95. data/lib/smith/workflow/parallel/execution_context.rb +113 -0
  96. data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
  97. data/lib/smith/workflow/parallel/root_execution.rb +91 -0
  98. data/lib/smith/workflow/parallel.rb +19 -22
  99. data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
  100. data/lib/smith/workflow/parallel_execution.rb +7 -3
  101. data/lib/smith/workflow/persistence.rb +49 -10
  102. data/lib/smith/workflow/prepared_step.rb +122 -0
  103. data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
  104. data/lib/smith/workflow/prepared_step_execution_authorization.rb +116 -0
  105. data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
  106. data/lib/smith/workflow/prepared_step_execution_scope.rb +45 -0
  107. data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
  108. data/lib/smith/workflow/retry_execution.rb +20 -11
  109. data/lib/smith/workflow/split_step_persistence/boundary.rb +128 -0
  110. data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
  111. data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
  112. data/lib/smith/workflow/split_step_persistence/checkpoint.rb +119 -0
  113. data/lib/smith/workflow/split_step_persistence/checkpoint_state.rb +51 -0
  114. data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
  115. data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
  116. data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
  117. data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
  118. data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
  119. data/lib/smith/workflow/split_step_persistence/execution.rb +128 -0
  120. data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +89 -0
  121. data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +90 -0
  122. data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +95 -0
  123. data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
  124. data/lib/smith/workflow/split_step_persistence/execution_verification.rb +65 -0
  125. data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
  126. data/lib/smith/workflow/split_step_persistence/inheritance.rb +22 -0
  127. data/lib/smith/workflow/split_step_persistence/payloads.rb +54 -0
  128. data/lib/smith/workflow/split_step_persistence/preparation.rb +125 -0
  129. data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +117 -0
  130. data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
  131. data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +54 -0
  132. data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
  133. data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
  134. data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
  135. data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
  136. data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
  137. data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +84 -0
  138. data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +102 -0
  139. data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
  140. data/lib/smith/workflow/split_step_persistence/transition_contract.rb +48 -0
  141. data/lib/smith/workflow/split_step_persistence/transition_contract_freezer.rb +83 -0
  142. data/lib/smith/workflow/split_step_persistence/transition_contract_signature.rb +107 -0
  143. data/lib/smith/workflow/split_step_persistence/transition_contract_structured_values.rb +51 -0
  144. data/lib/smith/workflow/split_step_persistence.rb +79 -0
  145. data/lib/smith/workflow/step_completion.rb +31 -0
  146. data/lib/smith/workflow/string_snapshot.rb +25 -0
  147. data/lib/smith/workflow/transition.rb +122 -133
  148. data/lib/smith/workflow/transition_actionability.rb +22 -0
  149. data/lib/smith/workflow.rb +57 -19
  150. data/lib/smith.rb +42 -2
  151. data/sig/smith.rbs +1 -0
  152. metadata +131 -19
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "transition_optimization_configuration"
6
+
7
+ module Smith
8
+ class Workflow
9
+ class Graph
10
+ class TransitionContractConfigurations
11
+ extend Dry::Initializer
12
+
13
+ param :transition
14
+ option :identifiers
15
+
16
+ def to_h
17
+ {
18
+ router_config: router_config,
19
+ optimization_config: optimization_config,
20
+ orchestrator_config: orchestrator_config,
21
+ fanout_config: fanout_config,
22
+ retry_config: retry_config
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ def router_config
29
+ config = transition.router_config
30
+ return unless config
31
+
32
+ {
33
+ routes: project_routes(config.fetch(:routes)),
34
+ confidence_threshold: config.fetch(:confidence_threshold),
35
+ fallback: identifiers.call(config.fetch(:fallback))
36
+ }.freeze
37
+ end
38
+
39
+ def optimization_config
40
+ config = transition.optimization_config
41
+ TransitionOptimizationConfiguration.new(config).to_h if config
42
+ end
43
+
44
+ def orchestrator_config
45
+ config = transition.orchestrator_config
46
+ return unless config
47
+
48
+ {
49
+ orchestrator: own(config.fetch(:orchestrator)),
50
+ worker: own(config.fetch(:worker)),
51
+ max_workers: config.fetch(:max_workers),
52
+ max_delegation_rounds: config.fetch(:max_delegation_rounds),
53
+ task_schema_label: label(config.fetch(:task_schema)),
54
+ worker_output_schema_label: label(config.fetch(:worker_output_schema)),
55
+ final_output_schema_label: label(config.fetch(:final_output_schema))
56
+ }.freeze
57
+ end
58
+
59
+ def fanout_config
60
+ branches = transition.fanout_config&.fetch(:branches, nil)
61
+ { branches: own_hash(branches) }.freeze if branches
62
+ end
63
+
64
+ def retry_config
65
+ config = transition.retry_config
66
+ return unless config
67
+
68
+ {
69
+ error_classes: config.fetch(:error_classes).dup.freeze,
70
+ attempts: config.fetch(:attempts),
71
+ backoff: config.fetch(:backoff),
72
+ max_delay: config[:max_delay],
73
+ jitter: config.fetch(:jitter)
74
+ }.freeze
75
+ end
76
+
77
+ def own_hash(hash)
78
+ hash.each_with_object({}) do |(key, value), copy|
79
+ copy[own(key)] = own(value)
80
+ end.freeze
81
+ end
82
+
83
+ def project_routes(routes)
84
+ routes.each_with_object({}) do |(key, transition_name), copy|
85
+ copy[own(key)] = identifiers.call(transition_name)
86
+ end.freeze
87
+ end
88
+
89
+ def own(value)
90
+ value.is_a?(String) ? value.dup.freeze : value
91
+ end
92
+
93
+ def label(value)
94
+ result = value.respond_to?(:name) && value.name && !value.name.empty? ? value.name : value.inspect
95
+ result.dup.freeze
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -17,7 +17,9 @@ module Smith
17
17
  transition_target_diagnostic(transition, :failure_transition, transition.failure_transition),
18
18
  *router_target_diagnostics(transition),
19
19
  *deterministic_route_diagnostics(transition),
20
- *target_state_mismatch_diagnostics(transition)
20
+ *target_state_mismatch_diagnostics(transition),
21
+ branch_limit_diagnostic(transition),
22
+ RetryPolicyDiagnostic.new(transition:).call
21
23
  ].compact
22
24
  end
23
25
  end
@@ -53,25 +55,65 @@ module Smith
53
55
  end
54
56
 
55
57
  def target_state_mismatch_diagnostics(transition)
56
- Targets.for(transition).filter_map do |target_name|
57
- target = graph.transitions[target_name]
58
- next unless target
59
- next if target.from.nil? || target.from == transition.to
60
-
61
- mismatch_diagnostic(transition, target_name, target)
58
+ success = successful_target_names(transition).filter_map do |target_name|
59
+ target_state_mismatch_diagnostic(transition, target_name, expected_state: transition.to)
62
60
  end
61
+ failure = target_state_mismatch_diagnostic(
62
+ transition,
63
+ transition.failure_transition,
64
+ expected_state: transition.from
65
+ )
66
+
67
+ [*success, failure].compact
63
68
  end
64
69
 
65
- def mismatch_diagnostic(transition, target_name, target)
70
+ def branch_limit_diagnostic(transition)
71
+ branch_count = static_branch_count(transition)
72
+ limit = Smith.config.parallel_branch_limit
73
+ return unless branch_count && branch_count > limit
74
+
66
75
  Diagnostic.new(
67
- severity: :warning,
76
+ severity: :error,
77
+ code: :parallel_branch_limit_exceeded,
78
+ transition: transition.name,
79
+ message: "Transition #{ref(transition.name)} declares #{branch_count} parallel branches, " \
80
+ "exceeding the configured limit #{limit}.",
81
+ suggestion: "Reduce the branch count or explicitly raise Smith.config.parallel_branch_limit."
82
+ )
83
+ end
84
+
85
+ def static_branch_count(transition)
86
+ return transition.fanout_config.fetch(:branches).length if transition.fanout?
87
+
88
+ transition.parallel_count if transition.parallel?
89
+ end
90
+
91
+ def successful_target_names(transition)
92
+ return Targets.router_for(transition) if transition.routed?
93
+
94
+ [transition.success_transition, *Array(transition.deterministic_routes)].compact.uniq
95
+ end
96
+
97
+ def target_state_mismatch_diagnostic(transition, target_name, expected_state:)
98
+ return if target_name.nil?
99
+
100
+ target = graph.transitions[target_name]
101
+ return unless target
102
+ return if target.from.nil? || target.from == expected_state
103
+
104
+ mismatch_diagnostic(transition, target_name, target, expected_state:)
105
+ end
106
+
107
+ def mismatch_diagnostic(transition, target_name, target, expected_state:)
108
+ Diagnostic.new(
109
+ severity: :error,
68
110
  code: :target_from_state_mismatch,
69
111
  transition: transition.name,
70
112
  target: target_name,
71
- state: transition.to,
113
+ state: expected_state,
72
114
  message: "Transition #{ref(transition.name)} can route to #{ref(target_name)}, " \
73
- "but #{ref(target_name)} starts from #{ref(target.from)} instead of #{ref(transition.to)}.",
74
- suggestion: "Align #{ref(target_name)}'s from state with #{ref(transition.to)}, " \
115
+ "but #{ref(target_name)} starts from #{ref(target.from)} instead of #{ref(expected_state)}.",
116
+ suggestion: "Align #{ref(target_name)}'s from state with #{ref(expected_state)}, " \
75
117
  "or remove the named route."
76
118
  )
77
119
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ module Smith
6
+ class Workflow
7
+ class Graph
8
+ class TransitionOptimizationConfiguration
9
+ extend Dry::Initializer
10
+
11
+ param :config
12
+
13
+ def to_h
14
+ identity
15
+ .merge(evaluation)
16
+ .merge(exit_modes)
17
+ .freeze
18
+ end
19
+
20
+ private
21
+
22
+ def identity
23
+ {
24
+ generator: own(config.fetch(:generator)),
25
+ evaluator: own(config.fetch(:evaluator)),
26
+ max_rounds: config.fetch(:max_rounds)
27
+ }
28
+ end
29
+
30
+ def evaluation
31
+ {
32
+ evaluator_schema_label: label(config.fetch(:evaluator_schema)),
33
+ evaluator_context: callable_marker(config[:evaluator_context]),
34
+ improvement_threshold: config[:improvement_threshold],
35
+ before_eval: callable_marker(config[:before_eval])
36
+ }
37
+ end
38
+
39
+ def exit_modes
40
+ {
41
+ on_exhaustion: callable_marker(config.fetch(:on_exhaustion)),
42
+ on_converged: callable_marker(config.fetch(:on_converged)),
43
+ on_threshold: callable_marker(config.fetch(:on_threshold))
44
+ }
45
+ end
46
+
47
+ def own(value)
48
+ value.is_a?(String) ? value.dup.freeze : value
49
+ end
50
+
51
+ def label(value)
52
+ result = value.respond_to?(:name) && value.name && !value.name.empty? ? value.name : value.inspect
53
+ result.dup.freeze
54
+ end
55
+
56
+ def callable_marker(value)
57
+ value.respond_to?(:call) ? :callable : own(value)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -26,6 +26,7 @@ module Smith
26
26
  option :from
27
27
  option :to
28
28
  option :kind
29
+ option :parallel_count, default: proc {}
29
30
  option :success_transition, default: proc {}
30
31
  option :failure_transition, default: proc {}
31
32
  option :routes, default: proc {}
@@ -46,7 +47,8 @@ module Smith
46
47
  name: transition.name,
47
48
  from: transition.from,
48
49
  to: transition.to,
49
- kind: kind_for(transition)
50
+ kind: kind_for(transition),
51
+ parallel_count: transition.parallel_count
50
52
  }.merge(routing_attributes(transition), contract_attributes(transition, workflow_class: workflow_class))
51
53
  end
52
54
 
@@ -109,6 +111,7 @@ module Smith
109
111
  from: from,
110
112
  to: to,
111
113
  kind: kind,
114
+ parallel_count: parallel_count,
112
115
  success_transition: success_transition,
113
116
  failure_transition: failure_transition,
114
117
  routes: routes,
@@ -35,7 +35,7 @@ module Smith
35
35
  end
36
36
 
37
37
  def reachable_transition_names
38
- @reachable_transition_names ||= Reachability.new(graph).transition_names
38
+ @reachable_transition_names ||= graph.reachable_transitions.map(&:name).freeze
39
39
  end
40
40
 
41
41
  def status_for(diagnostics)
@@ -3,13 +3,18 @@
3
3
  module Smith
4
4
  class Workflow
5
5
  class Graph
6
+ EMPTY_TRANSITIONS = [].freeze
7
+
6
8
  attr_reader :workflow_class, :initial_state, :states, :transitions
7
9
 
8
10
  def initialize(workflow_class:, initial_state:, states:, transitions:)
11
+ @identifier_projection = IdentifierProjection.new
9
12
  @workflow_class = workflow_class
10
- @initial_state = initial_state
11
- @states = Array(states).uniq.freeze
12
- @transitions = transitions.dup.freeze
13
+ @initial_state = project_identifier(initial_state)
14
+ @states = own_states(states)
15
+ @state_index = index_states
16
+ @transitions = own_transitions(transitions)
17
+ @transitions_by_state = build_transitions_by_state
13
18
  end
14
19
 
15
20
  def validate
@@ -25,13 +30,66 @@ module Smith
25
30
  TransitionSnapshot.from_transition(transition, workflow_class: workflow_class)
26
31
  end
27
32
  end
33
+
34
+ def transitions_from(state)
35
+ @transitions_by_state.fetch(state, EMPTY_TRANSITIONS)
36
+ end
37
+
38
+ def first_transition_from(state)
39
+ transitions_from(state).first
40
+ end
41
+
42
+ def reachable_transitions
43
+ @reachable_transitions ||= Reachability.new(self).each_transition.to_a.freeze
44
+ end
45
+
46
+ def state?(state)
47
+ @state_index.key?(state)
48
+ end
49
+
50
+ private
51
+
52
+ def project_identifier(identifier)
53
+ @identifier_projection.call(identifier)
54
+ end
55
+
56
+ def own_states(states)
57
+ Array(states).map { project_identifier(_1) }.uniq.freeze
58
+ end
59
+
60
+ def index_states
61
+ states.to_h { [_1, true] }.freeze
62
+ end
63
+
64
+ def own_transitions(transitions)
65
+ transitions.each_value.with_index.with_object({}) do |(transition, definition_index), snapshot|
66
+ contract = TransitionContract.from_transition(
67
+ transition,
68
+ identifiers: @identifier_projection,
69
+ definition_index:
70
+ )
71
+ snapshot[contract.name] = contract
72
+ end.freeze
73
+ end
74
+
75
+ def build_transitions_by_state
76
+ index = transitions.values.each_with_object({}) do |transition, result|
77
+ (result[transition.from] ||= []) << transition
78
+ end
79
+ index.each_value(&:freeze)
80
+ index.freeze
81
+ end
28
82
  end
29
83
  end
30
84
  end
31
85
 
32
86
  require_relative "graph/reference"
87
+ require_relative "graph/identifier_projection"
88
+ require_relative "graph/transition_contract"
33
89
  require_relative "graph/diagnostic"
34
90
  require_relative "graph/state_diagnostics"
91
+ require_relative "graph/targets"
92
+ require_relative "graph/execution_successors"
35
93
  require_relative "graph/reachability"
36
94
  require_relative "graph/reachability_diagnostics"
37
95
  require_relative "graph/metrics"
@@ -41,11 +99,11 @@ require_relative "graph/runtime_binding_diagnostic_builder"
41
99
  require_relative "graph/runtime_binding_diagnostics"
42
100
  require_relative "graph/runtime_readiness_report"
43
101
  require_relative "graph/runtime_readiness_metrics"
44
- require_relative "graph/targets"
45
102
  require_relative "graph/fanout_contract"
46
103
  require_relative "graph/optimization_contract"
47
104
  require_relative "graph/orchestration_contract"
48
105
  require_relative "graph/transition_snapshot"
106
+ require_relative "graph/retry_policy_diagnostic"
49
107
  require_relative "graph/transition_diagnostics"
50
108
  require_relative "graph/validator"
51
109
  require_relative "graph/runtime_readiness"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "transition_actionability"
4
+
3
5
  module Smith
4
6
  class Workflow
5
7
  module GuardrailIntegration
@@ -21,30 +23,42 @@ module Smith
21
23
  end
22
24
 
23
25
  def handle_step_failure(transition, error)
26
+ step = { transition: transition.name, from: transition.from, to: transition.to, error: error }
27
+ SplitStepPersistence
28
+ .instance_method(:capture_split_step_execution_result!)
29
+ .bind_call(self, step)
24
30
  failure_name = transition.failure_transition
25
31
  raise error unless failure_name
26
32
 
27
33
  fail_transition = self.class.find_transition(failure_name)
28
34
  raise error unless fail_transition
35
+
29
36
  validate_transition_origin!(fail_transition)
30
37
 
31
38
  if actionable_failure_transition?(fail_transition)
32
39
  @next_transition_name = failure_name
33
- return
40
+ return step
34
41
  end
35
42
 
36
43
  @state = fail_transition.to
44
+ step
45
+ end
46
+
47
+ def handle_unresolved_transition_failure(error)
48
+ fail_transition = self.class.find_transition(:fail)
49
+ raise error unless fail_transition
50
+
51
+ @outcome = nil
52
+ step = { transition: error.requested_name, from: @state, to: fail_transition.to, error: error }
53
+ SplitStepPersistence
54
+ .instance_method(:capture_split_step_execution_result!)
55
+ .bind_call(self, step)
56
+ @state = fail_transition.to
57
+ step
37
58
  end
38
59
 
39
60
  def actionable_failure_transition?(transition)
40
- transition.agent_name ||
41
- transition.deterministic? ||
42
- transition.routed? ||
43
- transition.fanout? ||
44
- transition.nested? ||
45
- transition.optimized? ||
46
- transition.orchestrated? ||
47
- transition.success_transition
61
+ TransitionActionability.call(transition)
48
62
  end
49
63
 
50
64
  def run_workflow_input_guardrails
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ class Identifier
6
+ def self.normalize(value, label:, allow_nil: false)
7
+ return if value.nil? && allow_nil
8
+
9
+ case value
10
+ when String
11
+ raise WorkflowError, "#{label} must not be blank" if value.strip.empty?
12
+
13
+ value.dup.freeze
14
+ when Symbol
15
+ raise WorkflowError, "#{label} must not be blank" if value.to_s.empty?
16
+
17
+ value
18
+ else
19
+ raise WorkflowError, "#{label} must be a String or Symbol"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "dry-struct"
5
+ require "json"
6
+
7
+ require_relative "../errors"
8
+ require_relative "../types"
9
+ require_relative "message_value_normalizer"
10
+
11
+ module Smith
12
+ class Workflow
13
+ class MessageAdmission < Dry::Struct
14
+ transform_keys(&:to_sym)
15
+ schema schema.strict
16
+
17
+ attribute :messages, Types::Array.of(Types::Hash)
18
+
19
+ attr_reader :message_digest
20
+
21
+ def initialize(attributes)
22
+ normalized = MessageValueNormalizer.new(attributes.fetch(:messages)).call
23
+ payload = JSON.generate(normalized, max_nesting: MessageValueNormalizer::MAX_DEPTH)
24
+ if payload.bytesize > MessageValueNormalizer::MAX_BYTES
25
+ raise WorkflowError,
26
+ "session message batch exceeds maximum bytes #{MessageValueNormalizer::MAX_BYTES}"
27
+ end
28
+
29
+ super(messages: normalized)
30
+ @message_digest = Digest::SHA256.hexdigest(payload).freeze
31
+ self.attributes.freeze
32
+ freeze
33
+ rescue JSON::GeneratorError, JSON::NestingError => e
34
+ raise WorkflowError, "session message batch is not canonical JSON: #{e.message}"
35
+ end
36
+
37
+ def message_count = messages.length
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "message_batch"
4
+
5
+ module Smith
6
+ class Workflow
7
+ module MessageAdmissionBoundary
8
+ def append_session_messages!(messages)
9
+ admission = MessageBatch.new(messages).call
10
+ @split_step_mutex.synchronize do
11
+ SplitStepPersistence.instance_method(:ensure_no_split_step_boundary!).bind_call(self)
12
+ unless @session_messages.nil? || @session_messages.is_a?(Array)
13
+ raise WorkflowError, "workflow session messages are not appendable"
14
+ end
15
+
16
+ (@session_messages ||= []).concat(admission.messages)
17
+ end
18
+ admission
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "../errors"
6
+ require_relative "message_admission"
7
+
8
+ module Smith
9
+ class Workflow
10
+ class MessageBatch
11
+ MAX_MESSAGES = 100
12
+ MAX_DEPTH = MessageValueNormalizer::MAX_DEPTH
13
+ MAX_NODES = MessageValueNormalizer::MAX_NODES
14
+ MAX_BYTES = MessageValueNormalizer::MAX_BYTES
15
+ ARRAY_EACH = Array.instance_method(:each)
16
+ private_constant :ARRAY_EACH
17
+
18
+ extend Dry::Initializer
19
+
20
+ param :messages
21
+
22
+ def call
23
+ batch = normalize_batch
24
+ batch.each { reject!("each session message must be a Hash") unless _1.is_a?(Hash) }
25
+ admission = MessageAdmission.new(messages: batch)
26
+ admission.messages.each { validate_message!(_1) }
27
+ admission
28
+ end
29
+
30
+ private
31
+
32
+ def normalize_batch
33
+ return [messages] if messages.is_a?(Hash)
34
+
35
+ reject!("session messages must be a message Hash or an Array of message Hashes") unless messages.is_a?(Array)
36
+
37
+ batch = []
38
+ ARRAY_EACH.bind_call(messages) do |message|
39
+ reject!("session message batch exceeds maximum count #{MAX_MESSAGES}") if batch.length == MAX_MESSAGES
40
+
41
+ batch << message
42
+ end
43
+ reject!("session message batch must not be empty") if batch.empty?
44
+
45
+ batch
46
+ end
47
+
48
+ def validate_message!(message)
49
+ reject!("each session message must be a Hash") unless message.is_a?(Hash)
50
+ role = message["role"]
51
+ reject!("each session message requires a non-empty role") unless
52
+ role.is_a?(String) && !role.strip.empty?
53
+ end
54
+
55
+ def reject!(message)
56
+ raise WorkflowError, message
57
+ end
58
+ end
59
+ end
60
+ end