smith-agents 0.4.5 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +201 -0
- data/README.md +108 -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 +24 -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 +6 -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/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 +16 -18
- 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 +1 -1
- 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 +133 -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 +7 -3
- data/lib/smith/workflow/persistence.rb +37 -8
- 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 +116 -0
- data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
- data/lib/smith/workflow/prepared_step_execution_scope.rb +45 -0
- data/lib/smith/workflow/prepared_step_recovery.rb +38 -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/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 +49 -47
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +89 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +90 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +65 -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 +50 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
- data/lib/smith/workflow/split_step_persistence.rb +36 -0
- data/lib/smith/workflow/step_completion.rb +31 -0
- data/lib/smith/workflow/string_snapshot.rb +25 -0
- data/lib/smith/workflow/transition.rb +122 -133
- data/lib/smith/workflow/transition_actionability.rb +22 -0
- data/lib/smith/workflow.rb +44 -18
- data/lib/smith.rb +41 -2
- data/sig/smith.rbs +1 -0
- metadata +110 -19
|
@@ -13,7 +13,11 @@ module Smith
|
|
|
13
13
|
handle_split_step_preparation_conflict!
|
|
14
14
|
raise
|
|
15
15
|
rescue StandardError
|
|
16
|
-
|
|
16
|
+
if @split_step_dispatch_started
|
|
17
|
+
mark_split_step_preparation_unknown!
|
|
18
|
+
else
|
|
19
|
+
reset_failed_split_step_preparation!
|
|
20
|
+
end
|
|
17
21
|
raise
|
|
18
22
|
end
|
|
19
23
|
|
|
@@ -37,7 +41,11 @@ module Smith
|
|
|
37
41
|
|
|
38
42
|
def mark_split_step_preparation_unknown!
|
|
39
43
|
@split_step_mutex.synchronize do
|
|
40
|
-
|
|
44
|
+
if @split_step_phase == :preparing
|
|
45
|
+
@split_step_phase = :preparation_unknown
|
|
46
|
+
@split_step_prepared_descriptor = nil
|
|
47
|
+
@split_step_transaction_identity = nil
|
|
48
|
+
end
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module Recovery
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def recover_prepared_step(recovery, adapter)
|
|
10
|
+
decision = validate_prepared_step_recovery!(recovery)
|
|
11
|
+
descriptor = decision.prepared_step
|
|
12
|
+
store = persistence_adapter!(adapter)
|
|
13
|
+
ensure_strict_split_step_persistence!
|
|
14
|
+
validate_split_step_adapter!(store)
|
|
15
|
+
ensure_recovery_outside_transaction!(store)
|
|
16
|
+
payload = fetch_recovery_payload!(store, descriptor.persistence_key)
|
|
17
|
+
document = validate_recovery_payload!(payload, decision, store)
|
|
18
|
+
restore_state(document, allow_step_in_progress: true)
|
|
19
|
+
attach_recovered_split_step!(descriptor, payload, store, dispatch_claim: decision.dispatch_claim)
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_prepared_step_recovery!(recovery)
|
|
24
|
+
return recovery if recovery.is_a?(PreparedStepRecovery)
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "recovery must be a Smith::Workflow::PreparedStepRecovery"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ensure_recovery_outside_transaction!(adapter)
|
|
30
|
+
return unless Smith::PersistenceAdapters.supports?(adapter, :transaction_open?)
|
|
31
|
+
return unless adapter.transaction_open?
|
|
32
|
+
|
|
33
|
+
raise WorkflowError, "prepared-step recovery requires committed host authority outside a transaction"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_recovery_payload!(adapter, key)
|
|
37
|
+
payload = adapter.fetch(key)
|
|
38
|
+
return payload if payload.is_a?(String)
|
|
39
|
+
|
|
40
|
+
raise WorkflowError, "the persisted split-step preparation is not available"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate_recovery_payload!(payload, decision, adapter)
|
|
44
|
+
if payload.bytesize > CanonicalPayloadDigest::MAX_BYTES
|
|
45
|
+
raise WorkflowError,
|
|
46
|
+
"persisted split-step preparation exceeds maximum bytes #{CanonicalPayloadDigest::MAX_BYTES}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
document = JSON.parse(payload)
|
|
50
|
+
raise WorkflowError, "the persisted split-step preparation must be an object" unless document.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
validate_recovery_definition!(document, decision.prepared_step)
|
|
53
|
+
validate_recovery_identity!(document, decision, adapter)
|
|
54
|
+
validate_recovery_digest!(payload, decision)
|
|
55
|
+
document
|
|
56
|
+
rescue JSON::ParserError, TypeError
|
|
57
|
+
raise WorkflowError, "the persisted split-step preparation is invalid"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_recovery_definition!(document, descriptor)
|
|
61
|
+
current_digest = self.class.definition_digest
|
|
62
|
+
return if recovery_digests_match?(document, descriptor, current_digest) &&
|
|
63
|
+
recovery_class_matches?(document) && recovery_schema_matches?(document)
|
|
64
|
+
|
|
65
|
+
raise WorkflowError, "the prepared step does not match the current workflow definition"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def recovery_digests_match?(document, descriptor, current_digest)
|
|
69
|
+
current_digest &&
|
|
70
|
+
descriptor.definition_digest == current_digest &&
|
|
71
|
+
document["definition_digest"] == current_digest
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def recovery_class_matches?(document)
|
|
75
|
+
!self.class.name.to_s.empty? && document["class"] == self.class.name
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def recovery_schema_matches?(document)
|
|
79
|
+
document["schema_version"] == self.class.persistence_schema_version
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_recovery_identity!(document, decision, adapter)
|
|
83
|
+
descriptor = decision.prepared_step
|
|
84
|
+
return if recovery_marker_matches?(document, decision, adapter) &&
|
|
85
|
+
recovery_position_matches?(document, descriptor)
|
|
86
|
+
|
|
87
|
+
raise WorkflowError, "the persisted split-step preparation does not match the recovery descriptor"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def recovery_marker_matches?(document, decision, adapter)
|
|
91
|
+
descriptor = decision.prepared_step
|
|
92
|
+
document["step_in_progress"] == true &&
|
|
93
|
+
recovery_phase_matches?(document, decision.dispatch_claim) &&
|
|
94
|
+
document["split_step_persistence_identity"] == adapter.persistence_identity &&
|
|
95
|
+
document["split_step_token"] == descriptor.token &&
|
|
96
|
+
document["persistence_key"] == descriptor.persistence_key &&
|
|
97
|
+
document["persistence_version"] == descriptor.persistence_version
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def recovery_phase_matches?(document, dispatch_claim)
|
|
101
|
+
if dispatch_claim
|
|
102
|
+
document["split_step_phase"] == "dispatching" &&
|
|
103
|
+
document["split_step_dispatch_token"] == dispatch_claim.token
|
|
104
|
+
else
|
|
105
|
+
document["split_step_phase"] == "prepared" && !document.key?("split_step_dispatch_token")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def recovery_position_matches?(document, descriptor)
|
|
110
|
+
step_count = document["step_count"]
|
|
111
|
+
step_count.is_a?(Integer) &&
|
|
112
|
+
step_count + 1 == descriptor.step_number &&
|
|
113
|
+
document["state"].to_s == descriptor.from
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_recovery_digest!(payload, decision)
|
|
117
|
+
expected = decision.dispatch_claim&.dispatch_digest || decision.prepared_step.preparation_digest
|
|
118
|
+
return if CanonicalPayloadDigest.call(payload) == expected
|
|
119
|
+
|
|
120
|
+
raise WorkflowError, "the persisted split-step recovery payload digest does not match"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module RecoveryBoundary
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def attach_recovered_split_step!(descriptor, payload, adapter, dispatch_claim:)
|
|
10
|
+
transition = pending_split_step_transition
|
|
11
|
+
transition_name = @next_transition_name || transition&.name
|
|
12
|
+
unless transition && transition_name.to_s == descriptor.transition && @state.to_s == descriptor.from
|
|
13
|
+
raise WorkflowError, "the recovered transition does not match the current workflow position"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
detach_split_step_execution_state!
|
|
17
|
+
install_recovered_split_step!(descriptor, payload, adapter, transition, dispatch_claim)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def install_recovered_split_step!(descriptor, payload, adapter, transition, dispatch_claim)
|
|
21
|
+
signature = TransitionContract.capture(transition)
|
|
22
|
+
@split_step_mutex.synchronize do
|
|
23
|
+
install_recovered_transition!(transition, signature)
|
|
24
|
+
install_recovered_persistence!(descriptor, payload, adapter)
|
|
25
|
+
install_recovered_dispatch!(dispatch_claim)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def install_recovered_transition!(transition, signature)
|
|
30
|
+
@split_step_transition_name = transition.name
|
|
31
|
+
@split_step_transition = transition
|
|
32
|
+
@split_step_transition_signature = signature
|
|
33
|
+
@split_step_origin_state = @state
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def install_recovered_persistence!(descriptor, payload, adapter)
|
|
37
|
+
@split_step_token = descriptor.token
|
|
38
|
+
@split_step_prepared_descriptor = descriptor
|
|
39
|
+
@split_step_persistence_key = descriptor.persistence_key
|
|
40
|
+
@split_step_adapter = adapter
|
|
41
|
+
@split_step_persistence_ttl = nil
|
|
42
|
+
@split_step_preparation_payload = payload.dup.freeze
|
|
43
|
+
@split_step_preparation_digest = descriptor.preparation_digest
|
|
44
|
+
@split_step_definition_digest = descriptor.definition_digest
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def install_recovered_dispatch!(dispatch_claim)
|
|
48
|
+
@split_step_dispatch_started = false
|
|
49
|
+
@split_step_dispatch_token = dispatch_claim&.token
|
|
50
|
+
@split_step_dispatch_descriptor = dispatch_claim
|
|
51
|
+
@split_step_phase = dispatch_claim ? :dispatch_claimed : :prepared
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module RecoveryClassMethods
|
|
7
|
+
def recover_prepared_step(recovery, adapter: Smith.persistence_adapter)
|
|
8
|
+
seal_definition_identity!
|
|
9
|
+
allocate.send(:recover_prepared_step, recovery, adapter)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
class ReplaceExactSignature
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
CONTRACT_KEYWORDS = %i[expected_payload ttl].freeze
|
|
12
|
+
|
|
13
|
+
param :callable
|
|
14
|
+
|
|
15
|
+
def valid? = accepts_two_values? && accepts_keywords?
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def parameters = callable.parameters
|
|
20
|
+
|
|
21
|
+
def accepts_two_values?
|
|
22
|
+
required = parameters.count { |kind, _| kind == :req }
|
|
23
|
+
positional = parameters.count { |kind, _| %i[req opt].include?(kind) }
|
|
24
|
+
rest = parameters.any? { |kind, _| kind == :rest }
|
|
25
|
+
required <= 2 && (positional >= 2 || rest)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def accepts_keywords?
|
|
29
|
+
required_keywords_supported? && contract_keywords_accepted?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def required_keywords_supported?
|
|
33
|
+
required = parameters.filter_map { |kind, name| name if kind == :keyreq }
|
|
34
|
+
(required - CONTRACT_KEYWORDS).empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def contract_keywords_accepted?
|
|
38
|
+
return true if parameters.any? { |kind, _| kind == :keyrest }
|
|
39
|
+
|
|
40
|
+
keywords = parameters.filter_map { |kind, name| name if %i[key keyreq].include?(kind) }
|
|
41
|
+
CONTRACT_KEYWORDS.all? { |name| keywords.include?(name) }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "replace_exact_signature"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
module RestartSafeAdapter
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def validate_restart_safe_adapter!(adapter)
|
|
12
|
+
return unless restart_safe_split_step?
|
|
13
|
+
|
|
14
|
+
ensure_replace_exact_capability!(adapter)
|
|
15
|
+
ensure_replace_exact_signature!(adapter)
|
|
16
|
+
ensure_persistence_identity!(adapter)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ensure_replace_exact_capability!(adapter)
|
|
20
|
+
return if Smith::PersistenceAdapters.supports?(adapter, :replace_exact)
|
|
21
|
+
|
|
22
|
+
raise WorkflowError, "restart-safe split-step persistence requires replace_exact"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ensure_replace_exact_signature!(adapter)
|
|
26
|
+
return if ReplaceExactSignature.new(adapter.method(:replace_exact)).valid?
|
|
27
|
+
|
|
28
|
+
raise WorkflowError,
|
|
29
|
+
"restart-safe split-step persistence requires replace_exact(key, payload, expected_payload:, ttl:)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ensure_persistence_identity!(adapter)
|
|
33
|
+
identity = adapter.persistence_identity if
|
|
34
|
+
Smith::PersistenceAdapters.supports?(adapter, :persistence_identity)
|
|
35
|
+
return if identity.is_a?(String) && !identity.empty? && identity.bytesize <= 256
|
|
36
|
+
|
|
37
|
+
raise WorkflowError, "restart-safe split-step persistence requires a bounded persistence_identity"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -4,6 +4,43 @@ module Smith
|
|
|
4
4
|
class Workflow
|
|
5
5
|
module SplitStepPersistence
|
|
6
6
|
module SubclassBoundary
|
|
7
|
+
class << self
|
|
8
|
+
def protect_execution_path!(workflow_class, *owners)
|
|
9
|
+
protected_method_names(owners).each do |method_name|
|
|
10
|
+
protect_method!(workflow_class, method_name)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def protected_method_names(owners)
|
|
17
|
+
owners.flat_map do |owner|
|
|
18
|
+
owner.private_instance_methods(false) + owner.protected_instance_methods(false)
|
|
19
|
+
end.uniq
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def protect_method!(workflow_class, method_name)
|
|
23
|
+
return if boundary_method_names.include?(method_name)
|
|
24
|
+
|
|
25
|
+
implementation = workflow_class.instance_method(method_name)
|
|
26
|
+
define_method(method_name) do |*arguments, **keywords, &block|
|
|
27
|
+
active = SubclassBoundary
|
|
28
|
+
.instance_method(:smith_prepared_execution_active?)
|
|
29
|
+
.bind_call(self)
|
|
30
|
+
if active
|
|
31
|
+
implementation.bind_call(self, *arguments, **keywords, &block)
|
|
32
|
+
else
|
|
33
|
+
super(*arguments, **keywords, &block)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
private method_name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def boundary_method_names
|
|
40
|
+
instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
7
44
|
def self.guard
|
|
8
45
|
Module.new.tap do |guard|
|
|
9
46
|
instance_methods(false).each do |method_name|
|
|
@@ -33,6 +70,15 @@ module Smith
|
|
|
33
70
|
|
|
34
71
|
private
|
|
35
72
|
|
|
73
|
+
def smith_prepared_execution_active?
|
|
74
|
+
root_execution = @split_step_phase == :executing &&
|
|
75
|
+
@split_step_execution_thread.equal?(Thread.current)
|
|
76
|
+
return true if root_execution
|
|
77
|
+
|
|
78
|
+
authorization = @split_step_active_execution_authorization
|
|
79
|
+
authorization&.active_in_current_execution? == true
|
|
80
|
+
end
|
|
81
|
+
|
|
36
82
|
def effective_persistence_ttl
|
|
37
83
|
return super unless split_step_boundary_active?
|
|
38
84
|
return super unless instance_variable_defined?(:@split_step_persistence_ttl)
|
|
@@ -46,6 +92,10 @@ module Smith
|
|
|
46
92
|
|
|
47
93
|
{ ttl: ttl }
|
|
48
94
|
end
|
|
95
|
+
|
|
96
|
+
def dispatch_store!(...)
|
|
97
|
+
DispatchBoundary.instance_method(:dispatch_store!).bind_call(self, ...)
|
|
98
|
+
end
|
|
49
99
|
end
|
|
50
100
|
end
|
|
51
101
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
class TransactionIdentity
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
param :adapter
|
|
12
|
+
|
|
13
|
+
def self.capture(adapter) = new(adapter).capture
|
|
14
|
+
def self.matches?(adapter, expected) = new(adapter).matches?(expected)
|
|
15
|
+
|
|
16
|
+
def capture
|
|
17
|
+
return unless PersistenceAdapters.supports?(adapter, :transaction_open?)
|
|
18
|
+
return unless adapter.transaction_open?
|
|
19
|
+
|
|
20
|
+
ensure_identity_capability!
|
|
21
|
+
identity = adapter.transaction_identity
|
|
22
|
+
if !identity || identity.to_s.empty?
|
|
23
|
+
raise WorkflowError, "transactional split-step persistence requires an active transaction identity"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
identity.to_s.dup.freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def matches?(expected)
|
|
30
|
+
capture == expected
|
|
31
|
+
rescue StandardError
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def ensure_identity_capability!
|
|
38
|
+
return if PersistenceAdapters.supports?(adapter, :transaction_identity)
|
|
39
|
+
|
|
40
|
+
raise WorkflowError, "transactional split-step persistence requires transaction_identity"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -4,17 +4,39 @@ require "digest"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "securerandom"
|
|
6
6
|
|
|
7
|
+
require_relative "prepared_step"
|
|
8
|
+
require_relative "prepared_step_dispatch"
|
|
9
|
+
require_relative "prepared_step_execution_authorization"
|
|
10
|
+
require_relative "prepared_step_execution_result"
|
|
11
|
+
require_relative "prepared_step_recovery"
|
|
7
12
|
require_relative "split_step_persistence/subclass_boundary"
|
|
8
13
|
require_relative "split_step_persistence/inheritance"
|
|
14
|
+
require_relative "split_step_persistence/recovery_class_methods"
|
|
9
15
|
require_relative "split_step_persistence/transition_contract_structured_values"
|
|
10
16
|
require_relative "split_step_persistence/transition_contract_signature"
|
|
11
17
|
require_relative "split_step_persistence/transition_contract_freezer"
|
|
12
18
|
require_relative "split_step_persistence/transition_contract"
|
|
19
|
+
require_relative "split_step_persistence/transaction_identity"
|
|
20
|
+
require_relative "split_step_persistence/canonical_payload_digest"
|
|
21
|
+
require_relative "split_step_persistence/definition_boundary"
|
|
13
22
|
require_relative "split_step_persistence/boundary"
|
|
23
|
+
require_relative "split_step_persistence/boundary_reset"
|
|
14
24
|
require_relative "split_step_persistence/state_snapshot"
|
|
15
25
|
require_relative "split_step_persistence/preparation_claim"
|
|
16
26
|
require_relative "split_step_persistence/preparation_recovery"
|
|
17
27
|
require_relative "split_step_persistence/preparation"
|
|
28
|
+
require_relative "split_step_persistence/restart_safe_adapter"
|
|
29
|
+
require_relative "split_step_persistence/recovery"
|
|
30
|
+
require_relative "split_step_persistence/recovery_boundary"
|
|
31
|
+
require_relative "split_step_persistence/preparation_payload"
|
|
32
|
+
require_relative "split_step_persistence/dispatch_boundary"
|
|
33
|
+
require_relative "split_step_persistence/dispatch_claim"
|
|
34
|
+
require_relative "split_step_persistence/dispatch_verification"
|
|
35
|
+
require_relative "split_step_persistence/dispatch_confirmation"
|
|
36
|
+
require_relative "split_step_persistence/execution_verification"
|
|
37
|
+
require_relative "split_step_persistence/execution_binding_snapshot"
|
|
38
|
+
require_relative "split_step_persistence/execution_authorization"
|
|
39
|
+
require_relative "split_step_persistence/execution_result_capture"
|
|
18
40
|
require_relative "split_step_persistence/execution"
|
|
19
41
|
require_relative "split_step_persistence/checkpoint_state"
|
|
20
42
|
require_relative "split_step_persistence/checkpoint"
|
|
@@ -27,13 +49,27 @@ module Smith
|
|
|
27
49
|
|
|
28
50
|
def self.prepended(base)
|
|
29
51
|
base.singleton_class.prepend(Inheritance)
|
|
52
|
+
base.extend(RecoveryClassMethods)
|
|
30
53
|
end
|
|
31
54
|
|
|
32
55
|
include Boundary
|
|
56
|
+
include BoundaryReset
|
|
57
|
+
include DefinitionBoundary
|
|
33
58
|
include StateSnapshot
|
|
34
59
|
include PreparationClaim
|
|
35
60
|
include PreparationRecovery
|
|
36
61
|
include Preparation
|
|
62
|
+
include RestartSafeAdapter
|
|
63
|
+
include Recovery
|
|
64
|
+
include RecoveryBoundary
|
|
65
|
+
include PreparationPayload
|
|
66
|
+
include DispatchBoundary
|
|
67
|
+
include DispatchClaim
|
|
68
|
+
include DispatchVerification
|
|
69
|
+
include DispatchConfirmation
|
|
70
|
+
include ExecutionVerification
|
|
71
|
+
include ExecutionAuthorization
|
|
72
|
+
include ExecutionResultCapture
|
|
37
73
|
include Execution
|
|
38
74
|
include CheckpointState
|
|
39
75
|
include Checkpoint
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module StepCompletion
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def complete_step(transition, output)
|
|
9
|
+
step = { transition: transition.name, from: transition.from, to: transition.to, output: output }
|
|
10
|
+
result = SplitStepPersistence
|
|
11
|
+
.instance_method(:prepare_split_step_execution_result)
|
|
12
|
+
.bind_call(self, step)
|
|
13
|
+
StepCompletion.instance_method(:append_accepted_output).bind_call(self, output)
|
|
14
|
+
SplitStepPersistence
|
|
15
|
+
.instance_method(:commit_split_step_execution_result!)
|
|
16
|
+
.bind_call(self, result)
|
|
17
|
+
@state = transition.to
|
|
18
|
+
@next_transition_name = @router_next_transition || transition.success_transition
|
|
19
|
+
@router_next_transition = nil
|
|
20
|
+
emit_step_completed(transition, output)
|
|
21
|
+
step
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def append_accepted_output(output)
|
|
25
|
+
return unless @session_messages && !output.nil?
|
|
26
|
+
|
|
27
|
+
@session_messages << { role: :assistant, content: output }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class StringSnapshot
|
|
6
|
+
BYTESIZE = String.instance_method(:bytesize)
|
|
7
|
+
FREEZE = String.instance_method(:freeze)
|
|
8
|
+
INITIALIZE_COPY = String.instance_method(:initialize_copy)
|
|
9
|
+
private_constant :BYTESIZE, :FREEZE, :INITIALIZE_COPY
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def bytesize(value)
|
|
13
|
+
BYTESIZE.bind_call(value)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def copy(value, freeze: false)
|
|
17
|
+
String.allocate.tap do |copy|
|
|
18
|
+
INITIALIZE_COPY.bind_call(copy, value)
|
|
19
|
+
FREEZE.bind_call(copy) if freeze
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|