smith-agents 0.4.5 → 0.6.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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +191 -4
  4. data/docs/CONFIGURATION.md +4 -1
  5. data/docs/PATTERNS.md +7 -0
  6. data/docs/PERSISTENCE.md +347 -25
  7. data/lib/smith/agent/lifecycle.rb +19 -10
  8. data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
  9. data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
  10. data/lib/smith/agent/registry.rb +8 -0
  11. data/lib/smith/agent.rb +37 -2
  12. data/lib/smith/budget/amount_contract.rb +67 -0
  13. data/lib/smith/budget/amount_representation.rb +43 -0
  14. data/lib/smith/budget/decimal_context.rb +18 -0
  15. data/lib/smith/budget/ledger.rb +110 -18
  16. data/lib/smith/budget/ledger_publication.rb +60 -0
  17. data/lib/smith/budget/ledger_state.rb +21 -0
  18. data/lib/smith/budget/ledger_state_transition.rb +72 -0
  19. data/lib/smith/budget/public_state_validator.rb +25 -0
  20. data/lib/smith/budget/reservation.rb +20 -0
  21. data/lib/smith/budget/reservation_contract.rb +34 -0
  22. data/lib/smith/context/observation_masking.rb +3 -1
  23. data/lib/smith/context/state_injection.rb +1 -1
  24. data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -4
  25. data/lib/smith/error.rb +5 -0
  26. data/lib/smith/errors.rb +10 -0
  27. data/lib/smith/exponential_backoff.rb +128 -0
  28. data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
  29. data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
  30. data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
  31. data/lib/smith/persistence_adapters/active_record_store.rb +7 -4
  32. data/lib/smith/persistence_adapters/cache_store.rb +1 -0
  33. data/lib/smith/persistence_adapters/memory.rb +16 -1
  34. data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
  35. data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
  36. data/lib/smith/persistence_adapters/redis_store.rb +28 -11
  37. data/lib/smith/persistence_adapters/retry.rb +22 -10
  38. data/lib/smith/persistence_adapters.rb +21 -18
  39. data/lib/smith/persistence_payload_conflict.rb +12 -0
  40. data/lib/smith/tool/budget_enforcement.rb +2 -2
  41. data/lib/smith/types.rb +1 -0
  42. data/lib/smith/version.rb +2 -1
  43. data/lib/smith/workflow/branch_env.rb +1 -1
  44. data/lib/smith/workflow/budget_integration.rb +21 -11
  45. data/lib/smith/workflow/composite/branch.rb +93 -0
  46. data/lib/smith/workflow/composite/branch_budget_contract.rb +41 -0
  47. data/lib/smith/workflow/composite/branch_contract.rb +102 -0
  48. data/lib/smith/workflow/composite/branch_execution.rb +124 -0
  49. data/lib/smith/workflow/composite/branch_failure.rb +91 -0
  50. data/lib/smith/workflow/composite/branch_outcome.rb +108 -0
  51. data/lib/smith/workflow/composite/budget_allocator.rb +78 -0
  52. data/lib/smith/workflow/composite/budget_math.rb +51 -0
  53. data/lib/smith/workflow/composite/contract.rb +100 -0
  54. data/lib/smith/workflow/composite/effects.rb +140 -0
  55. data/lib/smith/workflow/composite/effects_application.rb +36 -0
  56. data/lib/smith/workflow/composite/effects_baseline.rb +36 -0
  57. data/lib/smith/workflow/composite/effects_preflight.rb +102 -0
  58. data/lib/smith/workflow/composite/encoded_value_budget.rb +36 -0
  59. data/lib/smith/workflow/composite/enums.rb +27 -0
  60. data/lib/smith/workflow/composite/error.rb +54 -0
  61. data/lib/smith/workflow/composite/error_evidence.rb +61 -0
  62. data/lib/smith/workflow/composite/execution_contract.rb +73 -0
  63. data/lib/smith/workflow/composite/fanout_branch_contract.rb +61 -0
  64. data/lib/smith/workflow/composite/input.rb +53 -0
  65. data/lib/smith/workflow/composite/outcome_accumulator.rb +131 -0
  66. data/lib/smith/workflow/composite/outcome_set.rb +23 -0
  67. data/lib/smith/workflow/composite/payload.rb +128 -0
  68. data/lib/smith/workflow/composite/payload_digest.rb +28 -0
  69. data/lib/smith/workflow/composite/plan.rb +130 -0
  70. data/lib/smith/workflow/composite/plan_integrity.rb +52 -0
  71. data/lib/smith/workflow/composite/planner.rb +53 -0
  72. data/lib/smith/workflow/composite/preparation.rb +27 -0
  73. data/lib/smith/workflow/composite/reducer.rb +133 -0
  74. data/lib/smith/workflow/composite/reduction.rb +31 -0
  75. data/lib/smith/workflow/composite/value_budget.rb +90 -0
  76. data/lib/smith/workflow/composite_branch_execution_authorization.rb +52 -0
  77. data/lib/smith/workflow/deadline_enforcement.rb +22 -5
  78. data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
  79. data/lib/smith/workflow/definition_identity.rb +25 -0
  80. data/lib/smith/workflow/deterministic_step.rb +2 -2
  81. data/lib/smith/workflow/dsl.rb +73 -5
  82. data/lib/smith/workflow/durability.rb +1 -1
  83. data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
  84. data/lib/smith/workflow/execution.rb +28 -30
  85. data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
  86. data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
  87. data/lib/smith/workflow/fanout_execution.rb +20 -19
  88. data/lib/smith/workflow/graph/diagnostic.rb +27 -1
  89. data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
  90. data/lib/smith/workflow/graph/execution_successors.rb +65 -0
  91. data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
  92. data/lib/smith/workflow/graph/metrics.rb +1 -3
  93. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
  94. data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
  95. data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
  96. data/lib/smith/workflow/graph/reachability.rb +19 -31
  97. data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
  98. data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
  99. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
  100. data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
  101. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
  102. data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
  103. data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
  104. data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
  105. data/lib/smith/workflow/graph/transition_contract.rb +47 -0
  106. data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
  107. data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
  108. data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
  109. data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
  110. data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
  111. data/lib/smith/workflow/graph/validator.rb +1 -1
  112. data/lib/smith/workflow/graph.rb +62 -4
  113. data/lib/smith/workflow/guardrail_integration.rb +23 -9
  114. data/lib/smith/workflow/identifier.rb +24 -0
  115. data/lib/smith/workflow/message_admission.rb +40 -0
  116. data/lib/smith/workflow/message_admission_boundary.rb +22 -0
  117. data/lib/smith/workflow/message_batch.rb +60 -0
  118. data/lib/smith/workflow/message_value_normalizer.rb +134 -0
  119. data/lib/smith/workflow/nested_execution.rb +10 -0
  120. data/lib/smith/workflow/orchestrator_worker.rb +3 -7
  121. data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
  122. data/lib/smith/workflow/parallel/execution_context.rb +113 -0
  123. data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
  124. data/lib/smith/workflow/parallel/root_execution.rb +91 -0
  125. data/lib/smith/workflow/parallel.rb +19 -22
  126. data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
  127. data/lib/smith/workflow/parallel_execution.rb +46 -14
  128. data/lib/smith/workflow/persistence.rb +37 -8
  129. data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
  130. data/lib/smith/workflow/prepared_step.rb +122 -0
  131. data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
  132. data/lib/smith/workflow/prepared_step_execution_authorization.rb +101 -0
  133. data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
  134. data/lib/smith/workflow/prepared_step_execution_scope.rb +111 -0
  135. data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
  136. data/lib/smith/workflow/process_local.rb +33 -0
  137. data/lib/smith/workflow/retry_execution.rb +20 -11
  138. data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
  139. data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
  140. data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
  141. data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
  142. data/lib/smith/workflow/split_step_persistence/composite_branch_authorization.rb +99 -0
  143. data/lib/smith/workflow/split_step_persistence/composite_branch_effects.rb +41 -0
  144. data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +95 -0
  145. data/lib/smith/workflow/split_step_persistence/composite_branch_outcome.rb +36 -0
  146. data/lib/smith/workflow/split_step_persistence/composite_execution.rb +39 -0
  147. data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +57 -0
  148. data/lib/smith/workflow/split_step_persistence/composite_reduction_execution.rb +122 -0
  149. data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
  150. data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
  151. data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
  152. data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
  153. data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
  154. data/lib/smith/workflow/split_step_persistence/execution.rb +20 -64
  155. data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +109 -0
  156. data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
  157. data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +95 -0
  158. data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +109 -0
  159. data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
  160. data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
  161. data/lib/smith/workflow/split_step_persistence/execution_verification.rb +77 -0
  162. data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
  163. data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
  164. data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
  165. data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
  166. data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
  167. data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
  168. data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
  169. data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
  170. data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
  171. data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
  172. data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
  173. data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
  174. data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +66 -0
  175. data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
  176. data/lib/smith/workflow/split_step_persistence.rb +50 -0
  177. data/lib/smith/workflow/step_completion.rb +31 -0
  178. data/lib/smith/workflow/step_context.rb +46 -0
  179. data/lib/smith/workflow/string_snapshot.rb +25 -0
  180. data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
  181. data/lib/smith/workflow/transition.rb +137 -133
  182. data/lib/smith/workflow/transition_actionability.rb +22 -0
  183. data/lib/smith/workflow.rb +64 -18
  184. data/lib/smith.rb +44 -2
  185. data/sig/smith.rbs +1 -0
  186. metadata +155 -19
@@ -9,7 +9,8 @@ module Smith
9
9
  extend Dry::Initializer
10
10
 
11
11
  param :graph
12
- option :visited, default: proc {}
12
+ option :reports
13
+ option :cycle_transitions, default: proc { {}.compare_by_identity }
13
14
 
14
15
  def to_a
15
16
  cycle_diagnostics + report_entries.flat_map { |entry| diagnostics_for(entry) }
@@ -28,27 +29,30 @@ module Smith
28
29
  end
29
30
 
30
31
  def cycle_diagnostics
31
- nested_workflow_transitions.filter_map do |transition|
32
+ cycle_transitions.keys.map do |transition|
32
33
  workflow_class = transition.workflow_class
33
- cycle_diagnostic(transition, workflow_class) if visited_workflows.include?(workflow_class)
34
+ cycle_diagnostic(transition, workflow_class)
34
35
  end
35
36
  end
36
37
 
37
38
  def report_entries
38
39
  @report_entries ||= nested_workflow_transitions.filter_map do |transition|
40
+ next if cycle_transitions.key?(transition)
41
+
39
42
  workflow_class = transition.workflow_class
40
- next if visited_workflows.include?(workflow_class)
43
+ report = reports[workflow_class]
44
+ next unless report
41
45
 
42
46
  {
43
47
  transition: transition,
44
48
  workflow_class: workflow_class,
45
- report: workflow_class.graph.runtime_readiness(visited: visited_workflows)
49
+ report: report
46
50
  }.freeze
47
51
  end.freeze
48
52
  end
49
53
 
50
54
  def nested_workflow_transitions
51
- @nested_workflow_transitions ||= graph.transitions.values.select(&:nested?)
55
+ @nested_workflow_transitions ||= graph.reachable_transitions.select(&:nested?)
52
56
  end
53
57
 
54
58
  def cycle_diagnostic(transition, workflow_class)
@@ -67,21 +71,16 @@ module Smith
67
71
 
68
72
  def nested_diagnostic(transition, workflow_class, diagnostic)
69
73
  label = workflow_label(workflow_class)
74
+ code = diagnostic.code.to_s.start_with?("nested_") ? diagnostic.code : :"nested_#{diagnostic.code}"
70
75
 
71
- Diagnostic.new(
72
- severity: diagnostic.severity,
73
- code: :"nested_#{diagnostic.code}",
76
+ diagnostic.nested(
77
+ label: label,
78
+ code: code,
74
79
  transition: transition.name,
75
- target: label,
76
- message: "Nested workflow #{label}: #{diagnostic.message}",
77
- suggestion: diagnostic.suggestion
80
+ target: label
78
81
  )
79
82
  end
80
83
 
81
- def visited_workflows
82
- @visited_workflows ||= Set.new(Array(visited)).add(graph.workflow_class)
83
- end
84
-
85
84
  def ref(value)
86
85
  Reference.format(value)
87
86
  end
@@ -44,10 +44,10 @@ module Smith
44
44
 
45
45
  def evaluation_contract
46
46
  {
47
- evaluator_schema: label_for(config.fetch(:evaluator_schema)),
47
+ evaluator_schema: config.fetch(:evaluator_schema_label),
48
48
  evaluator_context: config[:evaluator_context],
49
49
  improvement_threshold: config[:improvement_threshold],
50
- before_eval: callable_label(config[:before_eval])
50
+ before_eval: config[:before_eval]
51
51
  }
52
52
  end
53
53
 
@@ -46,9 +46,9 @@ module Smith
46
46
 
47
47
  def schema_contract
48
48
  {
49
- task_schema: label_for(config.fetch(:task_schema)),
50
- worker_output_schema: label_for(config.fetch(:worker_output_schema)),
51
- final_output_schema: label_for(config.fetch(:final_output_schema))
49
+ task_schema: config.fetch(:task_schema_label),
50
+ worker_output_schema: config.fetch(:worker_output_schema_label),
51
+ final_output_schema: config.fetch(:final_output_schema_label)
52
52
  }
53
53
  end
54
54
 
@@ -11,47 +11,35 @@ module Smith
11
11
  end
12
12
 
13
13
  def transition_names
14
- return [] unless graph.initial_state
15
-
16
- reset_walk
17
- drain_queue
18
- @seen_transitions.keys
14
+ each_transition.map(&:name)
19
15
  end
20
16
 
21
- private
17
+ def each_transition
18
+ return enum_for(__method__) unless block_given?
22
19
 
23
- def reset_walk
24
- @seen_states = { graph.initial_state => true }
25
- @seen_transitions = {}
26
- @queue = [graph.initial_state]
27
- end
20
+ root = successors.initial_transition
21
+ return unless root
28
22
 
29
- def drain_queue
30
- transitions_from(@queue.shift).each { |transition| visit_transition(transition) } until @queue.empty?
23
+ walk(root) { yield _1 }
31
24
  end
32
25
 
33
- def visit_transition(transition)
34
- return if @seen_transitions.key?(transition.name)
35
-
36
- @seen_transitions[transition.name] = true
37
- enqueue_state(transition.to)
38
- Targets.for(transition).each { |target_name| visit_named_transition(target_name) }
39
- end
26
+ private
40
27
 
41
- def visit_named_transition(target_name)
42
- target = graph.transitions[target_name]
43
- visit_transition(target) if target
28
+ def successors
29
+ @successors ||= ExecutionSuccessors.new(graph)
44
30
  end
45
31
 
46
- def enqueue_state(state)
47
- return if state.nil? || @seen_states.key?(state)
48
-
49
- @seen_states[state] = true
50
- @queue << state
51
- end
32
+ def walk(root)
33
+ seen = {}
34
+ stack = [root]
35
+ until stack.empty?
36
+ transition = stack.pop
37
+ next if seen.key?(transition.name)
52
38
 
53
- def transitions_from(state)
54
- graph.transitions.values.select { |transition| transition.from == state }
39
+ seen[transition.name] = true
40
+ yield transition
41
+ successors.for(transition).reverse_each { stack << _1 }
42
+ end
55
43
  end
56
44
  end
57
45
  end
@@ -9,11 +9,13 @@ module Smith
9
9
  def initialize(graph, reachable_transition_names)
10
10
  @graph = graph
11
11
  @reachable_transition_names = reachable_transition_names
12
+ @reachable_transition_index = reachable_transition_names.to_h { [_1, true] }.freeze
13
+ @control_marker_index = control_marker_index
12
14
  end
13
15
 
14
16
  def to_a
15
17
  (graph.transitions.keys - reachable_transition_names).filter_map do |transition_name|
16
- next if auto_fail_transition?(transition_name)
18
+ next if auto_fail_transition?(transition_name) || control_marker?(transition_name)
17
19
 
18
20
  diagnostic_for(transition_name)
19
21
  end
@@ -25,6 +27,20 @@ module Smith
25
27
  transition_name == :fail && graph.transitions[transition_name]&.from.nil?
26
28
  end
27
29
 
30
+ def control_marker?(transition_name)
31
+ @control_marker_index.key?(transition_name)
32
+ end
33
+
34
+ def control_marker_index
35
+ successors = ExecutionSuccessors.new(graph)
36
+ graph.transitions.each_value.with_object({}) do |transition, index|
37
+ next unless @reachable_transition_index.key?(transition.name)
38
+
39
+ marker = successors.failure_control_marker(transition)
40
+ index[marker.name] = true if marker
41
+ end.freeze
42
+ end
43
+
28
44
  def diagnostic_for(transition_name)
29
45
  transition = graph.transitions.fetch(transition_name)
30
46
  Diagnostic.new(
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ module Smith
6
+ class Workflow
7
+ class Graph
8
+ class RetryPolicyDiagnostic
9
+ extend Dry::Initializer
10
+
11
+ option :transition
12
+
13
+ def call
14
+ attempts = transition.retry_config&.fetch(:attempts, nil)
15
+ limit = Smith.config.retry_attempt_limit
16
+ return unless attempts && attempts > limit
17
+
18
+ Diagnostic.new(
19
+ severity: :error,
20
+ code: :retry_attempt_limit_exceeded,
21
+ transition: transition.name,
22
+ message: "Transition #{ref(transition.name)} declares #{attempts} retry attempts, " \
23
+ "exceeding the configured limit #{limit}.",
24
+ suggestion: "Reduce the retry attempts or explicitly raise Smith.config.retry_attempt_limit."
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def ref(value)
31
+ Reference.format(value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -17,9 +17,9 @@ module Smith
17
17
  end
18
18
 
19
19
  def agent_bindings
20
- @agent_bindings ||= graph.transitions.values.flat_map do |transition|
20
+ @agent_bindings ||= graph.reachable_transitions.flat_map do |transition|
21
21
  bindings_for_transition(transition)
22
- end
22
+ end.freeze
23
23
  end
24
24
 
25
25
  private
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "dry-initializer"
4
4
 
5
+ require_relative "runtime_readiness_traversal"
6
+
5
7
  module Smith
6
8
  class Workflow
7
9
  class Graph
@@ -12,50 +14,7 @@ module Smith
12
14
  option :visited, default: proc {}
13
15
 
14
16
  def report
15
- RuntimeReadinessReport.new(
16
- workflow_class: workflow_label(graph.workflow_class),
17
- topology_report: topology_report,
18
- runtime_diagnostics: runtime_diagnostics,
19
- metrics: runtime_metrics
20
- )
21
- end
22
-
23
- private
24
-
25
- def topology_report
26
- @topology_report ||= graph.validate
27
- end
28
-
29
- def runtime_diagnostics
30
- @runtime_diagnostics ||= [
31
- *binding_diagnostics.to_a,
32
- *nested_diagnostics.to_a
33
- ]
34
- end
35
-
36
- def binding_diagnostics
37
- @binding_diagnostics ||= RuntimeBindingDiagnostics.new(graph)
38
- end
39
-
40
- def nested_diagnostics
41
- @nested_diagnostics ||= NestedReadinessDiagnostics.new(graph, visited: visited)
42
- end
43
-
44
- def runtime_metrics
45
- RuntimeReadinessMetrics.new(
46
- graph,
47
- topology_report: topology_report,
48
- runtime_diagnostics: runtime_diagnostics,
49
- agent_bindings: binding_diagnostics.agent_bindings,
50
- nested_reports: nested_diagnostics.child_reports
51
- ).to_h
52
- end
53
-
54
- def workflow_label(workflow_class)
55
- name = workflow_class.name
56
- return name if name && !name.empty?
57
-
58
- workflow_class.inspect
17
+ RuntimeReadinessTraversal.new(graph, visited:).report
59
18
  end
60
19
  end
61
20
  end
@@ -75,11 +75,11 @@ module Smith
75
75
  end
76
76
 
77
77
  def fanout_transitions
78
- @fanout_transitions ||= graph.transitions.values.select(&:fanout?)
78
+ @fanout_transitions ||= graph.reachable_transitions.select(&:fanout?)
79
79
  end
80
80
 
81
81
  def direct_nested_workflow_count
82
- graph.transitions.values.count(&:nested?)
82
+ graph.reachable_transitions.count(&:nested?)
83
83
  end
84
84
  end
85
85
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ module Smith
6
+ class Workflow
7
+ class Graph
8
+ class RuntimeReadinessReportBuilder
9
+ extend Dry::Initializer
10
+
11
+ param :graph
12
+ option :reports
13
+ option :cycle_transitions
14
+
15
+ def call
16
+ topology_report = graph.validate
17
+ bindings = RuntimeBindingDiagnostics.new(graph)
18
+ nested = NestedReadinessDiagnostics.new(graph, reports:, cycle_transitions:)
19
+ diagnostics = [*bindings.to_a, *nested.to_a]
20
+ RuntimeReadinessReport.new(
21
+ workflow_class: workflow_label,
22
+ topology_report: topology_report,
23
+ runtime_diagnostics: diagnostics,
24
+ metrics: metrics(topology_report, diagnostics, bindings, nested)
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def metrics(topology_report, diagnostics, bindings, nested)
31
+ RuntimeReadinessMetrics.new(
32
+ graph,
33
+ topology_report: topology_report,
34
+ runtime_diagnostics: diagnostics,
35
+ agent_bindings: bindings.agent_bindings,
36
+ nested_reports: nested.child_reports
37
+ ).to_h
38
+ end
39
+
40
+ def workflow_label
41
+ name = graph.workflow_class.name
42
+ name && !name.empty? ? name : graph.workflow_class.inspect
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "runtime_readiness_report_builder"
6
+
7
+ module Smith
8
+ class Workflow
9
+ class Graph
10
+ class RuntimeReadinessTraversal
11
+ extend Dry::Initializer
12
+
13
+ param :graph
14
+ option :visited, default: proc {}
15
+
16
+ def report
17
+ initialize_walk
18
+ inspect_graphs
19
+ @reports.fetch(graph.workflow_class)
20
+ end
21
+
22
+ private
23
+
24
+ def initialize_walk
25
+ @states = {}.compare_by_identity
26
+ @reports = {}.compare_by_identity
27
+ @graphs = {}.compare_by_identity
28
+ @nested_transitions = {}.compare_by_identity
29
+ @cycle_transitions = {}.compare_by_identity
30
+ visited_workflows.each { @states[_1] = :visiting }
31
+ @graphs[graph.workflow_class] = graph
32
+ @states[graph.workflow_class] = :visiting
33
+ end
34
+
35
+ def inspect_graphs
36
+ frames = [frame_for(graph)]
37
+ until frames.empty?
38
+ frame = frames.last
39
+ if frame.fetch(:index) >= frame.fetch(:transitions).length
40
+ complete_graph(frame.fetch(:graph))
41
+ frames.pop
42
+ next
43
+ end
44
+
45
+ transition = next_transition(frame)
46
+ visit_child(frame.fetch(:graph), transition, frames)
47
+ end
48
+ end
49
+
50
+ def frame_for(current_graph)
51
+ {
52
+ graph: current_graph,
53
+ transitions: nested_transitions_for(current_graph),
54
+ index: 0
55
+ }
56
+ end
57
+
58
+ def next_transition(frame)
59
+ transition = frame.fetch(:transitions).fetch(frame.fetch(:index))
60
+ frame[:index] += 1
61
+ transition
62
+ end
63
+
64
+ def visit_child(parent_graph, transition, frames)
65
+ workflow_class = transition.workflow_class
66
+ case @states[workflow_class]
67
+ when :visiting then mark_cycle(parent_graph, transition)
68
+ when :done then nil
69
+ else
70
+ child_graph = graph_for(workflow_class)
71
+ @states[workflow_class] = :visiting
72
+ frames << frame_for(child_graph)
73
+ end
74
+ end
75
+
76
+ def complete_graph(current_graph)
77
+ workflow_class = current_graph.workflow_class
78
+ @reports[workflow_class] = RuntimeReadinessReportBuilder.new(
79
+ current_graph,
80
+ reports: @reports,
81
+ cycle_transitions: cycles_for(current_graph)
82
+ ).call
83
+ @states[workflow_class] = :done
84
+ end
85
+
86
+ def graph_for(workflow_class)
87
+ @graphs[workflow_class] ||= workflow_class.graph
88
+ end
89
+
90
+ def nested_transitions_for(current_graph)
91
+ @nested_transitions[current_graph] ||= current_graph.reachable_transitions.select(&:nested?).freeze
92
+ end
93
+
94
+ def mark_cycle(current_graph, transition)
95
+ cycles_for(current_graph)[transition] = true
96
+ end
97
+
98
+ def cycles_for(current_graph)
99
+ @cycle_transitions[current_graph] ||= {}.compare_by_identity
100
+ end
101
+
102
+ def visited_workflows
103
+ visited.respond_to?(:keys) ? visited.keys : Array(visited)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -20,7 +20,7 @@ module Smith
20
20
  private
21
21
 
22
22
  def initial_state_diagnostics
23
- return [] if graph.initial_state && graph.states.include?(graph.initial_state)
23
+ return [] if graph.initial_state && graph.state?(graph.initial_state)
24
24
 
25
25
  [
26
26
  Diagnostic.new(
@@ -44,7 +44,7 @@ module Smith
44
44
 
45
45
  def undefined_state_diagnostic(transition, edge, state)
46
46
  return if edge == :from && state.nil?
47
- return if graph.states.include?(state)
47
+ return if graph.state?(state)
48
48
 
49
49
  Diagnostic.new(
50
50
  severity: :error,
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "transition_contract_attributes"
6
+
7
+ module Smith
8
+ class Workflow
9
+ class Graph
10
+ class TransitionContract
11
+ extend Dry::Initializer
12
+
13
+ option :name
14
+ option :definition_index
15
+ option :from
16
+ option :to
17
+ option :agent_name, default: proc {}
18
+ option :success_transition, default: proc {}
19
+ option :failure_transition, default: proc {}
20
+ option :router_config, default: proc {}
21
+ option :workflow_class, default: proc {}
22
+ option :optimization_config, default: proc {}
23
+ option :orchestrator_config, default: proc {}
24
+ option :fanout_config, default: proc {}
25
+ option :retry_config, default: proc {}
26
+ option :deterministic_kind, default: proc {}
27
+ option :deterministic_routes, default: proc {}
28
+ option :deterministic, default: proc { false }
29
+ option :parallel, default: proc { false }
30
+ option :parallel_count, default: proc {}
31
+
32
+ def self.from_transition(transition, identifiers:, definition_index:)
33
+ attributes = TransitionContractAttributes.new(transition, identifiers:).to_h
34
+ new(**attributes, definition_index:).freeze
35
+ end
36
+
37
+ def deterministic? = deterministic
38
+ def orchestrated? = !orchestrator_config.nil?
39
+ def fanout? = !fanout_config.nil?
40
+ def optimized? = !optimization_config.nil?
41
+ def nested? = !workflow_class.nil?
42
+ def routed? = !router_config.nil?
43
+ def parallel? = parallel
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "transition_contract_configurations"
6
+
7
+ module Smith
8
+ class Workflow
9
+ class Graph
10
+ class TransitionContractAttributes
11
+ extend Dry::Initializer
12
+
13
+ param :transition
14
+ option :identifiers
15
+
16
+ def to_h
17
+ identity_attributes
18
+ .merge(routing_attributes)
19
+ .merge(kind_attributes)
20
+ .merge(TransitionContractConfigurations.new(transition, identifiers:).to_h)
21
+ end
22
+
23
+ private
24
+
25
+ def identity_attributes
26
+ {
27
+ name: identifiers.call(transition.name),
28
+ from: identifiers.call(transition.from),
29
+ to: identifiers.call(transition.to),
30
+ agent_name: own(transition.agent_name),
31
+ workflow_class: transition.workflow_class
32
+ }
33
+ end
34
+
35
+ def routing_attributes
36
+ {
37
+ success_transition: project(transition.success_transition),
38
+ failure_transition: project(transition.failure_transition),
39
+ deterministic_routes: project_array(transition.deterministic_routes)
40
+ }
41
+ end
42
+
43
+ def kind_attributes
44
+ {
45
+ deterministic_kind: transition.deterministic_kind,
46
+ deterministic: transition.deterministic?,
47
+ parallel: transition.parallel?,
48
+ parallel_count: static_parallel_count
49
+ }
50
+ end
51
+
52
+ def static_parallel_count
53
+ return unless transition.parallel?
54
+
55
+ count = transition.agent_opts[:count]
56
+ count.respond_to?(:call) ? nil : (count || 1)
57
+ end
58
+
59
+ def project_array(array)
60
+ array&.map { identifiers.call(_1) }&.freeze
61
+ end
62
+
63
+ def project(value)
64
+ identifiers.call(value) unless value.nil?
65
+ end
66
+
67
+ def own(value)
68
+ value.is_a?(String) ? value.dup.freeze : value
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end