smith-agents 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +91 -3
- data/README.md +69 -3
- data/UPSTREAM_PROPOSAL.md +19 -0
- data/docs/PATTERNS.md +85 -6
- data/lib/smith/agent/lifecycle.rb +11 -7
- data/lib/smith/agent/registry/introspection.rb +27 -0
- data/lib/smith/agent/registry.rb +22 -17
- data/lib/smith/agent/registry_binding.rb +43 -0
- data/lib/smith/agent.rb +3 -4
- data/lib/smith/doctor/checks/models_registry.rb +25 -14
- data/lib/smith/doctor/checks/persistence_capabilities.rb +37 -29
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/agent_result.rb +26 -0
- data/lib/smith/workflow/branch_env.rb +24 -0
- data/lib/smith/workflow/budget_integration.rb +27 -5
- data/lib/smith/workflow/deterministic_execution.rb +1 -1
- data/lib/smith/workflow/deterministic_step.rb +32 -4
- data/lib/smith/workflow/durability.rb +38 -12
- data/lib/smith/workflow/evaluator_optimizer.rb +18 -10
- data/lib/smith/workflow/event_integration.rb +3 -3
- data/lib/smith/workflow/execution.rb +8 -1
- data/lib/smith/workflow/fanout_execution.rb +121 -0
- data/lib/smith/workflow/graph/contract_helpers.rb +65 -0
- data/lib/smith/workflow/graph/fanout_contract.rb +140 -0
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +98 -0
- data/lib/smith/workflow/graph/optimization_contract.rb +96 -0
- data/lib/smith/workflow/graph/orchestration_contract.rb +83 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb +124 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +114 -0
- data/lib/smith/workflow/graph/runtime_readiness.rb +63 -0
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +87 -0
- data/lib/smith/workflow/graph/runtime_readiness_report.rb +65 -0
- data/lib/smith/workflow/graph/targets.rb +5 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +7 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +80 -18
- data/lib/smith/workflow/graph.rb +16 -1
- data/lib/smith/workflow/graph_dsl.rb +4 -0
- data/lib/smith/workflow/guardrail_integration.rb +45 -10
- data/lib/smith/workflow/nested_execution.rb +5 -4
- data/lib/smith/workflow/optimization_state.rb +13 -0
- data/lib/smith/workflow/orchestration_state.rb +13 -0
- data/lib/smith/workflow/orchestrator_worker.rb +5 -16
- data/lib/smith/workflow/parallel/cancellation.rb +9 -0
- data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
- data/lib/smith/workflow/parallel.rb +12 -16
- data/lib/smith/workflow/parallel_execution.rb +5 -1
- data/lib/smith/workflow/persistence.rb +37 -6
- data/lib/smith/workflow/retry_execution.rb +52 -0
- data/lib/smith/workflow/router.rb +15 -4
- data/lib/smith/workflow/run_result.rb +54 -0
- data/lib/smith/workflow/transition.rb +260 -24
- data/lib/smith/workflow/usage_entry.rb +30 -0
- data/lib/smith/workflow/worker_execution.rb +13 -0
- data/lib/smith/workflow.rb +40 -136
- data/lib/smith.rb +12 -0
- metadata +24 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
class RuntimeBindingDiagnosticBuilder
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
param :graph
|
|
12
|
+
option :binding
|
|
13
|
+
|
|
14
|
+
def to_diagnostic
|
|
15
|
+
registry_binding = Agent::Registry.binding_for(agent)
|
|
16
|
+
return unresolved_agent_diagnostic unless registry_binding
|
|
17
|
+
|
|
18
|
+
if registry_binding.fetch(:agent_class).nil?
|
|
19
|
+
return uninspectable_agent_diagnostic(registry_binding) if registry_binding.fetch(:call)
|
|
20
|
+
|
|
21
|
+
return invalid_agent_diagnostic(registry_binding.fetch(:raw_binding))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
agent_class = registry_binding.fetch(:agent_class)
|
|
25
|
+
return if agent_class.model_configured?
|
|
26
|
+
return required_model_diagnostic if model_required?
|
|
27
|
+
|
|
28
|
+
model_warning
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def unresolved_agent_diagnostic
|
|
34
|
+
Diagnostic.new(
|
|
35
|
+
severity: :error,
|
|
36
|
+
code: :unresolved_agent_binding,
|
|
37
|
+
transition: transition.name,
|
|
38
|
+
target: agent,
|
|
39
|
+
message: "Transition #{ref(transition.name)} references unregistered #{role} #{ref(agent)}.",
|
|
40
|
+
suggestion: "Load and register agent #{ref(agent)} before running workflow #{workflow_label}."
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def invalid_agent_diagnostic(raw_binding)
|
|
45
|
+
Diagnostic.new(
|
|
46
|
+
severity: :error,
|
|
47
|
+
code: :invalid_agent_binding,
|
|
48
|
+
transition: transition.name,
|
|
49
|
+
target: agent,
|
|
50
|
+
message: "Transition #{ref(transition.name)} references #{role} #{ref(agent)}, " \
|
|
51
|
+
"but the registry binding is not a Smith::Agent subclass.",
|
|
52
|
+
suggestion: "Register #{ref(agent)} as a Smith::Agent subclass instead of " \
|
|
53
|
+
"#{raw_binding.class}."
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def uninspectable_agent_diagnostic(registry_binding)
|
|
58
|
+
Diagnostic.new(
|
|
59
|
+
severity: model_required? ? :error : :warning,
|
|
60
|
+
code: :uninspectable_agent_binding,
|
|
61
|
+
transition: transition.name,
|
|
62
|
+
target: agent,
|
|
63
|
+
message: "Transition #{ref(transition.name)} references #{role} #{ref(agent)}, " \
|
|
64
|
+
"but the registry binding is lazy and cannot be inspected without resolving it.",
|
|
65
|
+
suggestion: "Register #{ref(registry_binding.fetch(:key))} as a concrete Smith::Agent subclass " \
|
|
66
|
+
"before relying on static runtime readiness."
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def required_model_diagnostic
|
|
71
|
+
Diagnostic.new(
|
|
72
|
+
severity: :error,
|
|
73
|
+
code: :agent_without_required_model,
|
|
74
|
+
transition: transition.name,
|
|
75
|
+
target: agent,
|
|
76
|
+
message: "Transition #{ref(transition.name)} references #{role} #{ref(agent)}, " \
|
|
77
|
+
"but that role requires model output at runtime.",
|
|
78
|
+
suggestion: "Configure a model for agent #{ref(agent)} or move this transition to a " \
|
|
79
|
+
"deterministic/nested workflow pattern."
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def model_warning
|
|
84
|
+
Diagnostic.new(
|
|
85
|
+
severity: :warning,
|
|
86
|
+
code: :agent_without_model,
|
|
87
|
+
transition: transition.name,
|
|
88
|
+
target: agent,
|
|
89
|
+
message: "Transition #{ref(transition.name)} references #{role} #{ref(agent)}, " \
|
|
90
|
+
"but the registered agent has no model configured.",
|
|
91
|
+
suggestion: "Configure a model for agent #{ref(agent)} if this transition should call a provider."
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def agent
|
|
96
|
+
binding.fetch(:agent)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def transition
|
|
100
|
+
binding.fetch(:transition)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def role
|
|
104
|
+
binding.fetch(:role)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def model_required?
|
|
108
|
+
binding.fetch(:requires_model)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ref(value)
|
|
112
|
+
Reference.format(value)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def workflow_label
|
|
116
|
+
name = graph.workflow_class.name
|
|
117
|
+
return name if name && !name.empty?
|
|
118
|
+
|
|
119
|
+
graph.workflow_class.inspect
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
class RuntimeBindingDiagnostics
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
param :graph
|
|
12
|
+
|
|
13
|
+
def to_a
|
|
14
|
+
agent_bindings.filter_map do |binding|
|
|
15
|
+
RuntimeBindingDiagnosticBuilder.new(graph, binding: binding).to_diagnostic
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def agent_bindings
|
|
20
|
+
@agent_bindings ||= graph.transitions.values.flat_map do |transition|
|
|
21
|
+
bindings_for_transition(transition)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def bindings_for_transition(transition)
|
|
28
|
+
[
|
|
29
|
+
primary_agent_binding(transition),
|
|
30
|
+
*optimizer_agent_bindings(transition),
|
|
31
|
+
*orchestrator_agent_bindings(transition),
|
|
32
|
+
*fanout_agent_bindings(transition)
|
|
33
|
+
].compact
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def primary_agent_binding(transition)
|
|
37
|
+
return unless transition.agent_name
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
transition: transition,
|
|
41
|
+
role: agent_role_for(transition),
|
|
42
|
+
agent: transition.agent_name,
|
|
43
|
+
requires_model: transition.routed?
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def optimizer_agent_bindings(transition)
|
|
48
|
+
config = transition.optimization_config
|
|
49
|
+
return [] unless config
|
|
50
|
+
|
|
51
|
+
[
|
|
52
|
+
{
|
|
53
|
+
transition: transition,
|
|
54
|
+
role: "optimizer generator",
|
|
55
|
+
agent: config.fetch(:generator),
|
|
56
|
+
requires_model: true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
transition: transition,
|
|
60
|
+
role: "optimizer evaluator",
|
|
61
|
+
agent: config.fetch(:evaluator),
|
|
62
|
+
requires_model: true
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def orchestrator_agent_bindings(transition)
|
|
68
|
+
config = transition.orchestrator_config
|
|
69
|
+
return [] unless config
|
|
70
|
+
|
|
71
|
+
[
|
|
72
|
+
{
|
|
73
|
+
transition: transition,
|
|
74
|
+
role: "orchestrator",
|
|
75
|
+
agent: config.fetch(:orchestrator),
|
|
76
|
+
requires_model: true
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
transition: transition,
|
|
80
|
+
role: "worker",
|
|
81
|
+
agent: config.fetch(:worker),
|
|
82
|
+
requires_model: true
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def fanout_agent_bindings(transition)
|
|
88
|
+
branches = transition.fanout_config&.fetch(:branches, nil)
|
|
89
|
+
return [] unless branches
|
|
90
|
+
|
|
91
|
+
branches.map do |branch, agent|
|
|
92
|
+
{
|
|
93
|
+
transition: transition,
|
|
94
|
+
role: "fan-out branch #{ref(branch)}",
|
|
95
|
+
agent: agent,
|
|
96
|
+
requires_model: false
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def agent_role_for(transition)
|
|
102
|
+
return "router agent" if transition.routed?
|
|
103
|
+
return "parallel agent" if transition.parallel?
|
|
104
|
+
|
|
105
|
+
"agent"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def ref(value)
|
|
109
|
+
Reference.format(value)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
class RuntimeReadiness
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
param :graph
|
|
12
|
+
option :visited, default: proc {}
|
|
13
|
+
|
|
14
|
+
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
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
class RuntimeReadinessMetrics
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
param :graph
|
|
12
|
+
option :topology_report
|
|
13
|
+
option :runtime_diagnostics
|
|
14
|
+
option :agent_bindings
|
|
15
|
+
option :nested_reports, default: proc { [] }
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
core_metrics.merge(
|
|
19
|
+
binding_metrics,
|
|
20
|
+
nested_workflow_metrics,
|
|
21
|
+
fanout_metrics
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def core_metrics
|
|
28
|
+
{
|
|
29
|
+
topology_status: topology_report.status,
|
|
30
|
+
unresolved_agent_bindings_count: diagnostic_count(:unresolved_agent_binding),
|
|
31
|
+
invalid_agent_bindings_count: diagnostic_count(:invalid_agent_binding),
|
|
32
|
+
uninspectable_agent_bindings_count: diagnostic_count(:uninspectable_agent_binding),
|
|
33
|
+
modelless_agent_bindings_count: diagnostic_count(:agent_without_model),
|
|
34
|
+
required_model_missing_count: diagnostic_count(:agent_without_required_model)
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def binding_metrics
|
|
39
|
+
{
|
|
40
|
+
direct_agent_bindings_count: agent_bindings.length,
|
|
41
|
+
agent_bindings_count: agent_bindings.length + nested_metric_sum(:agent_bindings_count)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def nested_workflow_metrics
|
|
46
|
+
{
|
|
47
|
+
direct_nested_workflow_count: direct_nested_workflow_count,
|
|
48
|
+
nested_workflow_count: direct_nested_workflow_count + nested_metric_sum(:nested_workflow_count)
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fanout_metrics
|
|
53
|
+
{
|
|
54
|
+
direct_fanout_groups_count: fanout_transitions.length,
|
|
55
|
+
fanout_groups_count: fanout_transitions.length + nested_metric_sum(:fanout_groups_count),
|
|
56
|
+
direct_fanout_branches_count: fanout_branch_count,
|
|
57
|
+
fanout_branches_count: fanout_branch_count + nested_metric_sum(:fanout_branches_count)
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def nested_metric_sum(key)
|
|
62
|
+
nested_reports.sum { |report| report.metrics.fetch(key, 0) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def diagnostic_count(code)
|
|
66
|
+
runtime_diagnostics.count do |diagnostic|
|
|
67
|
+
diagnostic.code.to_s.end_with?(code.to_s)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fanout_branch_count
|
|
72
|
+
fanout_transitions.sum do |transition|
|
|
73
|
+
transition.fanout_config.fetch(:branches).length
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fanout_transitions
|
|
78
|
+
@fanout_transitions ||= graph.transitions.values.select(&:fanout?)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def direct_nested_workflow_count
|
|
82
|
+
graph.transitions.values.count(&:nested?)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
class RuntimeReadinessReport
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
option :workflow_class
|
|
12
|
+
option :topology_report
|
|
13
|
+
option :runtime_diagnostics
|
|
14
|
+
option :metrics
|
|
15
|
+
|
|
16
|
+
def ready?
|
|
17
|
+
errors.empty?
|
|
18
|
+
end
|
|
19
|
+
alias valid? ready?
|
|
20
|
+
|
|
21
|
+
def status
|
|
22
|
+
return :not_ready if errors.any?
|
|
23
|
+
return :warning if warnings.any?
|
|
24
|
+
|
|
25
|
+
:ready
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def topology_status
|
|
29
|
+
topology_report.status
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def diagnostics
|
|
33
|
+
topology_report.diagnostics + runtime_diagnostics
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def errors
|
|
37
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :error }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warnings
|
|
41
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :warning }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def suggestions
|
|
45
|
+
diagnostics.map(&:suggestion).compact.uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
{
|
|
50
|
+
status: status,
|
|
51
|
+
workflow_class: workflow_class,
|
|
52
|
+
topology_status: topology_status,
|
|
53
|
+
ready: ready?,
|
|
54
|
+
diagnostics: diagnostics.map(&:to_h),
|
|
55
|
+
runtime_diagnostics: runtime_diagnostics.map(&:to_h),
|
|
56
|
+
topology_diagnostics: topology_report.diagnostics.map(&:to_h),
|
|
57
|
+
suggestions: suggestions,
|
|
58
|
+
metrics: metrics,
|
|
59
|
+
graph: topology_report.to_h
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -21,6 +21,7 @@ module Smith
|
|
|
21
21
|
def names
|
|
22
22
|
names = [transition.success_transition, transition.failure_transition]
|
|
23
23
|
names.concat(router_names)
|
|
24
|
+
names.concat(deterministic_route_names)
|
|
24
25
|
names.compact.uniq
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -32,6 +33,10 @@ module Smith
|
|
|
32
33
|
transition.router_config.fetch(:fallback)
|
|
33
34
|
]
|
|
34
35
|
end
|
|
36
|
+
|
|
37
|
+
def deterministic_route_names
|
|
38
|
+
transition.deterministic_routes || []
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
end
|
|
37
42
|
end
|
|
@@ -16,6 +16,7 @@ module Smith
|
|
|
16
16
|
transition_target_diagnostic(transition, :success_transition, transition.success_transition),
|
|
17
17
|
transition_target_diagnostic(transition, :failure_transition, transition.failure_transition),
|
|
18
18
|
*router_target_diagnostics(transition),
|
|
19
|
+
*deterministic_route_diagnostics(transition),
|
|
19
20
|
*target_state_mismatch_diagnostics(transition)
|
|
20
21
|
].compact
|
|
21
22
|
end
|
|
@@ -45,6 +46,12 @@ module Smith
|
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
def deterministic_route_diagnostics(transition)
|
|
50
|
+
Array(transition.deterministic_routes).filter_map do |target|
|
|
51
|
+
transition_target_diagnostic(transition, :deterministic_route, target)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
48
55
|
def target_state_mismatch_diagnostics(transition)
|
|
49
56
|
Targets.for(transition).filter_map do |target_name|
|
|
50
57
|
target = graph.transitions[target_name]
|
|
@@ -1,31 +1,98 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "fanout_contract"
|
|
6
|
+
require_relative "optimization_contract"
|
|
7
|
+
require_relative "orchestration_contract"
|
|
8
|
+
|
|
3
9
|
module Smith
|
|
4
10
|
class Workflow
|
|
5
11
|
class Graph
|
|
6
12
|
class TransitionSnapshot
|
|
13
|
+
extend Dry::Initializer
|
|
14
|
+
|
|
7
15
|
KINDS = [
|
|
8
16
|
%i[deterministic deterministic?],
|
|
9
17
|
%i[router routed?],
|
|
10
18
|
%i[nested_workflow nested?],
|
|
11
19
|
%i[optimizer optimized?],
|
|
12
20
|
%i[orchestrator orchestrated?],
|
|
21
|
+
%i[fanout fanout?],
|
|
13
22
|
%i[parallel parallel?]
|
|
14
23
|
].freeze
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
option :name
|
|
26
|
+
option :from
|
|
27
|
+
option :to
|
|
28
|
+
option :kind
|
|
29
|
+
option :success_transition, default: proc {}
|
|
30
|
+
option :failure_transition, default: proc {}
|
|
31
|
+
option :routes, default: proc {}
|
|
32
|
+
option :fallback, default: proc {}
|
|
33
|
+
option :deterministic_routes, default: proc {}
|
|
34
|
+
option :fanout_branches, default: proc {}
|
|
35
|
+
option :fanout, default: proc {}
|
|
36
|
+
option :optimization, default: proc {}
|
|
37
|
+
option :orchestration, default: proc {}
|
|
38
|
+
option :retry_policy, default: proc {}
|
|
17
39
|
|
|
18
|
-
def self.from_transition(transition)
|
|
19
|
-
new(
|
|
40
|
+
def self.from_transition(transition, workflow_class: nil)
|
|
41
|
+
new(**attributes_for(transition, workflow_class: workflow_class))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.attributes_for(transition, workflow_class:)
|
|
45
|
+
{
|
|
20
46
|
name: transition.name,
|
|
21
47
|
from: transition.from,
|
|
22
48
|
to: transition.to,
|
|
23
|
-
kind: kind_for(transition)
|
|
49
|
+
kind: kind_for(transition)
|
|
50
|
+
}.merge(routing_attributes(transition), contract_attributes(transition, workflow_class: workflow_class))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.routing_attributes(transition)
|
|
54
|
+
{
|
|
24
55
|
success_transition: transition.success_transition,
|
|
25
56
|
failure_transition: transition.failure_transition,
|
|
26
57
|
routes: transition.router_config&.fetch(:routes, nil),
|
|
27
|
-
fallback: transition.router_config&.fetch(:fallback, nil)
|
|
28
|
-
|
|
58
|
+
fallback: transition.router_config&.fetch(:fallback, nil),
|
|
59
|
+
deterministic_routes: transition.deterministic_routes,
|
|
60
|
+
fanout_branches: transition.fanout_config&.fetch(:branches, nil)
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.contract_attributes(transition, workflow_class:)
|
|
65
|
+
{
|
|
66
|
+
fanout: fanout_for(transition, workflow_class: workflow_class),
|
|
67
|
+
optimization: optimization_for(transition, workflow_class: workflow_class),
|
|
68
|
+
orchestration: orchestration_for(transition, workflow_class: workflow_class),
|
|
69
|
+
retry_policy: retry_policy_for(transition)
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.fanout_for(transition, workflow_class:)
|
|
74
|
+
FanoutContract.from_transition(transition, workflow_class: workflow_class)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.optimization_for(transition, workflow_class:)
|
|
78
|
+
OptimizationContract.from_transition(transition, workflow_class: workflow_class)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.orchestration_for(transition, workflow_class:)
|
|
82
|
+
OrchestrationContract.from_transition(transition, workflow_class: workflow_class)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.retry_policy_for(transition)
|
|
86
|
+
config = transition.retry_config
|
|
87
|
+
return unless config
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
attempts: config.fetch(:attempts),
|
|
91
|
+
error_classes: config.fetch(:error_classes).map(&:name),
|
|
92
|
+
backoff: config.fetch(:backoff),
|
|
93
|
+
max_delay: config[:max_delay],
|
|
94
|
+
jitter: config.fetch(:jitter)
|
|
95
|
+
}.compact
|
|
29
96
|
end
|
|
30
97
|
|
|
31
98
|
def self.kind_for(transition)
|
|
@@ -36,17 +103,6 @@ module Smith
|
|
|
36
103
|
:noop
|
|
37
104
|
end
|
|
38
105
|
|
|
39
|
-
def initialize(**attributes)
|
|
40
|
-
@name = attributes.fetch(:name)
|
|
41
|
-
@from = attributes.fetch(:from)
|
|
42
|
-
@to = attributes.fetch(:to)
|
|
43
|
-
@kind = attributes.fetch(:kind)
|
|
44
|
-
@success_transition = attributes[:success_transition]
|
|
45
|
-
@failure_transition = attributes[:failure_transition]
|
|
46
|
-
@routes = attributes[:routes]
|
|
47
|
-
@fallback = attributes[:fallback]
|
|
48
|
-
end
|
|
49
|
-
|
|
50
106
|
def to_h
|
|
51
107
|
{
|
|
52
108
|
name: name,
|
|
@@ -56,7 +112,13 @@ module Smith
|
|
|
56
112
|
success_transition: success_transition,
|
|
57
113
|
failure_transition: failure_transition,
|
|
58
114
|
routes: routes,
|
|
59
|
-
fallback: fallback
|
|
115
|
+
fallback: fallback,
|
|
116
|
+
deterministic_routes: deterministic_routes,
|
|
117
|
+
fanout_branches: fanout_branches,
|
|
118
|
+
fanout: fanout,
|
|
119
|
+
optimization: optimization,
|
|
120
|
+
orchestration: orchestration,
|
|
121
|
+
retry_policy: retry_policy
|
|
60
122
|
}.compact
|
|
61
123
|
end
|
|
62
124
|
end
|
data/lib/smith/workflow/graph.rb
CHANGED
|
@@ -16,8 +16,14 @@ module Smith
|
|
|
16
16
|
Validator.new(self).report
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def runtime_readiness(visited: nil)
|
|
20
|
+
RuntimeReadiness.new(self, visited: visited).report
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
def transition_snapshots
|
|
20
|
-
transitions.values.map
|
|
24
|
+
transitions.values.map do |transition|
|
|
25
|
+
TransitionSnapshot.from_transition(transition, workflow_class: workflow_class)
|
|
26
|
+
end
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
end
|
|
@@ -29,8 +35,17 @@ require_relative "graph/state_diagnostics"
|
|
|
29
35
|
require_relative "graph/reachability"
|
|
30
36
|
require_relative "graph/reachability_diagnostics"
|
|
31
37
|
require_relative "graph/metrics"
|
|
38
|
+
require_relative "graph/nested_readiness_diagnostics"
|
|
32
39
|
require_relative "graph/report"
|
|
40
|
+
require_relative "graph/runtime_binding_diagnostic_builder"
|
|
41
|
+
require_relative "graph/runtime_binding_diagnostics"
|
|
42
|
+
require_relative "graph/runtime_readiness_report"
|
|
43
|
+
require_relative "graph/runtime_readiness_metrics"
|
|
33
44
|
require_relative "graph/targets"
|
|
45
|
+
require_relative "graph/fanout_contract"
|
|
46
|
+
require_relative "graph/optimization_contract"
|
|
47
|
+
require_relative "graph/orchestration_contract"
|
|
34
48
|
require_relative "graph/transition_snapshot"
|
|
35
49
|
require_relative "graph/transition_diagnostics"
|
|
36
50
|
require_relative "graph/validator"
|
|
51
|
+
require_relative "graph/runtime_readiness"
|