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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +264 -0
- data/README.md +191 -4
- data/docs/CONFIGURATION.md +4 -1
- data/docs/PATTERNS.md +7 -0
- data/docs/PERSISTENCE.md +347 -25
- data/lib/smith/agent/lifecycle.rb +19 -10
- data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
- data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
- data/lib/smith/agent/registry.rb +8 -0
- data/lib/smith/agent.rb +37 -2
- data/lib/smith/budget/amount_contract.rb +67 -0
- data/lib/smith/budget/amount_representation.rb +43 -0
- data/lib/smith/budget/decimal_context.rb +18 -0
- data/lib/smith/budget/ledger.rb +110 -18
- data/lib/smith/budget/ledger_publication.rb +60 -0
- data/lib/smith/budget/ledger_state.rb +21 -0
- data/lib/smith/budget/ledger_state_transition.rb +72 -0
- data/lib/smith/budget/public_state_validator.rb +25 -0
- data/lib/smith/budget/reservation.rb +20 -0
- data/lib/smith/budget/reservation_contract.rb +34 -0
- data/lib/smith/context/observation_masking.rb +3 -1
- data/lib/smith/context/state_injection.rb +1 -1
- data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -4
- data/lib/smith/error.rb +5 -0
- data/lib/smith/errors.rb +10 -0
- data/lib/smith/exponential_backoff.rb +128 -0
- data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
- data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
- data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +7 -4
- data/lib/smith/persistence_adapters/cache_store.rb +1 -0
- data/lib/smith/persistence_adapters/memory.rb +16 -1
- data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
- data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
- data/lib/smith/persistence_adapters/redis_store.rb +28 -11
- data/lib/smith/persistence_adapters/retry.rb +22 -10
- data/lib/smith/persistence_adapters.rb +21 -18
- data/lib/smith/persistence_payload_conflict.rb +12 -0
- data/lib/smith/tool/budget_enforcement.rb +2 -2
- data/lib/smith/types.rb +1 -0
- data/lib/smith/version.rb +2 -1
- data/lib/smith/workflow/branch_env.rb +1 -1
- data/lib/smith/workflow/budget_integration.rb +21 -11
- data/lib/smith/workflow/composite/branch.rb +93 -0
- data/lib/smith/workflow/composite/branch_budget_contract.rb +41 -0
- data/lib/smith/workflow/composite/branch_contract.rb +102 -0
- data/lib/smith/workflow/composite/branch_execution.rb +124 -0
- data/lib/smith/workflow/composite/branch_failure.rb +91 -0
- data/lib/smith/workflow/composite/branch_outcome.rb +108 -0
- data/lib/smith/workflow/composite/budget_allocator.rb +78 -0
- data/lib/smith/workflow/composite/budget_math.rb +51 -0
- data/lib/smith/workflow/composite/contract.rb +100 -0
- data/lib/smith/workflow/composite/effects.rb +140 -0
- data/lib/smith/workflow/composite/effects_application.rb +36 -0
- data/lib/smith/workflow/composite/effects_baseline.rb +36 -0
- data/lib/smith/workflow/composite/effects_preflight.rb +102 -0
- data/lib/smith/workflow/composite/encoded_value_budget.rb +36 -0
- data/lib/smith/workflow/composite/enums.rb +27 -0
- data/lib/smith/workflow/composite/error.rb +54 -0
- data/lib/smith/workflow/composite/error_evidence.rb +61 -0
- data/lib/smith/workflow/composite/execution_contract.rb +73 -0
- data/lib/smith/workflow/composite/fanout_branch_contract.rb +61 -0
- data/lib/smith/workflow/composite/input.rb +53 -0
- data/lib/smith/workflow/composite/outcome_accumulator.rb +131 -0
- data/lib/smith/workflow/composite/outcome_set.rb +23 -0
- data/lib/smith/workflow/composite/payload.rb +128 -0
- data/lib/smith/workflow/composite/payload_digest.rb +28 -0
- data/lib/smith/workflow/composite/plan.rb +130 -0
- data/lib/smith/workflow/composite/plan_integrity.rb +52 -0
- data/lib/smith/workflow/composite/planner.rb +53 -0
- data/lib/smith/workflow/composite/preparation.rb +27 -0
- data/lib/smith/workflow/composite/reducer.rb +133 -0
- data/lib/smith/workflow/composite/reduction.rb +31 -0
- data/lib/smith/workflow/composite/value_budget.rb +90 -0
- data/lib/smith/workflow/composite_branch_execution_authorization.rb +52 -0
- data/lib/smith/workflow/deadline_enforcement.rb +22 -5
- data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
- data/lib/smith/workflow/definition_identity.rb +25 -0
- data/lib/smith/workflow/deterministic_step.rb +2 -2
- data/lib/smith/workflow/dsl.rb +73 -5
- data/lib/smith/workflow/durability.rb +1 -1
- data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
- data/lib/smith/workflow/execution.rb +28 -30
- data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
- data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
- data/lib/smith/workflow/fanout_execution.rb +20 -19
- data/lib/smith/workflow/graph/diagnostic.rb +27 -1
- data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
- data/lib/smith/workflow/graph/execution_successors.rb +65 -0
- data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
- data/lib/smith/workflow/graph/metrics.rb +1 -3
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
- data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
- data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
- data/lib/smith/workflow/graph/reachability.rb +19 -31
- data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
- data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
- data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
- data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/transition_contract.rb +47 -0
- data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
- data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
- data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
- data/lib/smith/workflow/graph/validator.rb +1 -1
- data/lib/smith/workflow/graph.rb +62 -4
- data/lib/smith/workflow/guardrail_integration.rb +23 -9
- data/lib/smith/workflow/identifier.rb +24 -0
- data/lib/smith/workflow/message_admission.rb +40 -0
- data/lib/smith/workflow/message_admission_boundary.rb +22 -0
- data/lib/smith/workflow/message_batch.rb +60 -0
- data/lib/smith/workflow/message_value_normalizer.rb +134 -0
- data/lib/smith/workflow/nested_execution.rb +10 -0
- data/lib/smith/workflow/orchestrator_worker.rb +3 -7
- data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
- data/lib/smith/workflow/parallel/execution_context.rb +113 -0
- data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
- data/lib/smith/workflow/parallel/root_execution.rb +91 -0
- data/lib/smith/workflow/parallel.rb +19 -22
- data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
- data/lib/smith/workflow/parallel_execution.rb +46 -14
- data/lib/smith/workflow/persistence.rb +37 -8
- data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
- data/lib/smith/workflow/prepared_step.rb +122 -0
- data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
- data/lib/smith/workflow/prepared_step_execution_authorization.rb +101 -0
- data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
- data/lib/smith/workflow/prepared_step_execution_scope.rb +111 -0
- data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
- data/lib/smith/workflow/process_local.rb +33 -0
- data/lib/smith/workflow/retry_execution.rb +20 -11
- data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
- data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
- data/lib/smith/workflow/split_step_persistence/composite_branch_authorization.rb +99 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_effects.rb +41 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_outcome.rb +36 -0
- data/lib/smith/workflow/split_step_persistence/composite_execution.rb +39 -0
- data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/composite_reduction_execution.rb +122 -0
- data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +20 -64
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +109 -0
- data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +109 -0
- data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
- data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +77 -0
- data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
- data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
- data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
- data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
- data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
- data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
- data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
- data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
- data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
- data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +66 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
- data/lib/smith/workflow/split_step_persistence.rb +50 -0
- data/lib/smith/workflow/step_completion.rb +31 -0
- data/lib/smith/workflow/step_context.rb +46 -0
- data/lib/smith/workflow/string_snapshot.rb +25 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
- data/lib/smith/workflow/transition.rb +137 -133
- data/lib/smith/workflow/transition_actionability.rb +22 -0
- data/lib/smith/workflow.rb +64 -18
- data/lib/smith.rb +44 -2
- data/sig/smith.rbs +1 -0
- metadata +155 -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
|
-
|
|
57
|
-
|
|
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
|
|
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: :
|
|
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:
|
|
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(
|
|
74
|
-
suggestion: "Align #{ref(target_name)}'s from state with #{ref(
|
|
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,
|
data/lib/smith/workflow/graph.rb
CHANGED
|
@@ -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 =
|
|
12
|
-
@
|
|
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
|
|
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
|