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,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "process_local"
|
|
5
|
+
|
|
6
|
+
module Smith
|
|
7
|
+
class Workflow
|
|
8
|
+
class PreparedStepExecutionScope
|
|
9
|
+
include ProcessLocal
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@phase = :issued
|
|
14
|
+
@thread = nil
|
|
15
|
+
@fiber = nil
|
|
16
|
+
@branch_fibers = {}.compare_by_identity
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def activate!(thread, fiber = Fiber.current)
|
|
20
|
+
Thread.handle_interrupt(Object => :never) do
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
raise WorkflowError, "prepared-step execution scope is no longer available" unless @phase == :issued
|
|
23
|
+
|
|
24
|
+
@phase = :active
|
|
25
|
+
@thread = thread
|
|
26
|
+
@fiber = fiber
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def close!(thread = nil, fiber = Fiber.current)
|
|
32
|
+
Thread.handle_interrupt(Object => :never) do
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
if @phase == :active && thread && !owner?(@thread, @fiber, thread, fiber)
|
|
35
|
+
raise WorkflowError, "prepared-step execution scope belongs to another thread or fiber"
|
|
36
|
+
end
|
|
37
|
+
raise WorkflowError, "prepared-step execution still has active branch fibers" unless @branch_fibers.empty?
|
|
38
|
+
|
|
39
|
+
@phase = :closed
|
|
40
|
+
@thread = nil
|
|
41
|
+
@fiber = nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def within_branch(&block)
|
|
47
|
+
thread = Thread.current
|
|
48
|
+
fiber = Fiber.current
|
|
49
|
+
Thread.handle_interrupt(Object => :never) do
|
|
50
|
+
enter_branch!(thread, fiber)
|
|
51
|
+
begin
|
|
52
|
+
Thread.handle_interrupt(Object => :immediate, &block)
|
|
53
|
+
ensure
|
|
54
|
+
leave_branch!(thread, fiber)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def active_for?(thread, fiber = Fiber.current)
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
@phase == :active && (owner?(@thread, @fiber, thread, fiber) || branch_owner?(thread, fiber))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def binding_accessible_for?(thread, fiber = Fiber.current)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
@phase == :issued ||
|
|
68
|
+
(@phase == :active && (owner?(@thread, @fiber, thread, fiber) || branch_owner?(thread, fiber)))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def enter_branch!(thread, fiber)
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
raise WorkflowError, "prepared-step execution scope is not active" unless @phase == :active
|
|
77
|
+
|
|
78
|
+
entry = @branch_fibers[fiber]
|
|
79
|
+
if entry && !entry.fetch(:thread).equal?(thread)
|
|
80
|
+
raise WorkflowError, "prepared-step branch fiber belongs to another thread"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@branch_fibers[fiber] = { thread:, count: entry ? entry.fetch(:count) + 1 : 1 }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def leave_branch!(thread, fiber)
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
entry = @branch_fibers.fetch(fiber) do
|
|
90
|
+
raise WorkflowError, "prepared-step branch execution fiber is not active"
|
|
91
|
+
end
|
|
92
|
+
unless entry.fetch(:thread).equal?(thread)
|
|
93
|
+
raise WorkflowError, "prepared-step branch fiber belongs to another thread"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
count = entry.fetch(:count)
|
|
97
|
+
count == 1 ? @branch_fibers.delete(fiber) : entry[:count] = count - 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def branch_owner?(thread, fiber)
|
|
102
|
+
entry = @branch_fibers[fiber]
|
|
103
|
+
!!(entry && entry.fetch(:thread).equal?(thread))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def owner?(owner_thread, owner_fiber, thread, fiber)
|
|
107
|
+
owner_thread.equal?(thread) && owner_fiber.equal?(fiber)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-struct"
|
|
4
|
+
|
|
5
|
+
require_relative "../types"
|
|
6
|
+
require_relative "prepared_step"
|
|
7
|
+
require_relative "prepared_step_dispatch"
|
|
8
|
+
|
|
9
|
+
module Smith
|
|
10
|
+
class Workflow
|
|
11
|
+
class PreparedStepRecovery < Dry::Struct
|
|
12
|
+
attribute :prepared_step, Types.Instance(PreparedStep)
|
|
13
|
+
attribute? :dispatch_claim, Types.Instance(PreparedStepDispatch).optional
|
|
14
|
+
attribute :execution_status, Types::Symbol.enum(:not_started)
|
|
15
|
+
|
|
16
|
+
def self.not_started(witness)
|
|
17
|
+
case witness
|
|
18
|
+
when PreparedStep
|
|
19
|
+
new(prepared_step: witness, dispatch_claim: nil, execution_status: :not_started)
|
|
20
|
+
when PreparedStepDispatch
|
|
21
|
+
new(prepared_step: witness.prepared_step, dispatch_claim: witness, execution_status: :not_started)
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "recovery witness must be a PreparedStep or PreparedStepDispatch"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(attributes)
|
|
28
|
+
super
|
|
29
|
+
if dispatch_claim && dispatch_claim.prepared_step.to_h != prepared_step.to_h
|
|
30
|
+
raise ArgumentError, "dispatch claim must belong to the prepared step"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
self.attributes.freeze
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module ProcessLocal
|
|
6
|
+
def initialize_copy(_source)
|
|
7
|
+
raise TypeError, "#{self.class} cannot be copied"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def _dump(_depth)
|
|
11
|
+
raise TypeError, "#{self.class} cannot be serialized"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def encode_with(_coder)
|
|
15
|
+
raise TypeError, "#{self.class} cannot be serialized"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def init_with(_coder)
|
|
19
|
+
raise TypeError, "#{self.class} cannot be deserialized"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def as_json(*)
|
|
23
|
+
raise TypeError, "#{self.class} cannot be serialized"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_json(*)
|
|
27
|
+
raise TypeError, "#{self.class} cannot be serialized"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private_constant :ProcessLocal
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -9,6 +9,7 @@ module Smith
|
|
|
9
9
|
config = transition.retry_config
|
|
10
10
|
return run_guarded_step(transition) unless config
|
|
11
11
|
|
|
12
|
+
schedule = retry_schedule(config)
|
|
12
13
|
attempt = 0
|
|
13
14
|
begin
|
|
14
15
|
attempt += 1
|
|
@@ -16,7 +17,7 @@ module Smith
|
|
|
16
17
|
rescue StandardError => e
|
|
17
18
|
raise unless retry_transition_error?(config, e, attempt)
|
|
18
19
|
|
|
19
|
-
sleep_for_retry(
|
|
20
|
+
sleep_for_retry(schedule, attempt)
|
|
20
21
|
retry
|
|
21
22
|
end
|
|
22
23
|
end
|
|
@@ -32,20 +33,28 @@ module Smith
|
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def sleep_for_retry(
|
|
36
|
-
delay = retry_delay(
|
|
36
|
+
def sleep_for_retry(schedule, failed_attempt)
|
|
37
|
+
delay = retry_delay(schedule, failed_attempt)
|
|
37
38
|
sleep(delay) if delay.positive?
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
def retry_delay(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
def retry_delay(config_or_schedule, failed_attempt)
|
|
42
|
+
schedule = if config_or_schedule.is_a?(ExponentialBackoff)
|
|
43
|
+
config_or_schedule
|
|
44
|
+
else
|
|
45
|
+
retry_schedule(config_or_schedule)
|
|
46
|
+
end
|
|
47
|
+
schedule.delay(failed_attempt, random: method(:rand))
|
|
48
|
+
end
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
def retry_schedule(config)
|
|
51
|
+
ExponentialBackoff.new(
|
|
52
|
+
attempts: config.fetch(:attempts),
|
|
53
|
+
base_delay: config.fetch(:backoff),
|
|
54
|
+
max_delay: config[:max_delay],
|
|
55
|
+
jitter: config.fetch(:jitter),
|
|
56
|
+
delay_label: "backoff"
|
|
57
|
+
)
|
|
49
58
|
end
|
|
50
59
|
end
|
|
51
60
|
end
|
|
@@ -21,19 +21,50 @@ module Smith
|
|
|
21
21
|
|
|
22
22
|
def initialize_copy(source)
|
|
23
23
|
super
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
copied_split_step_phase = @split_step_phase
|
|
25
|
+
detach_copied_runtime_state!(source)
|
|
26
|
+
return unless copied_split_step_phase
|
|
26
27
|
|
|
27
28
|
@split_step_phase = :copied_boundary
|
|
29
|
+
reset_copied_execution_state!
|
|
30
|
+
reset_copied_dispatch_state!
|
|
31
|
+
end
|
|
32
|
+
private :initialize_copy
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def reset_copied_execution_state!
|
|
28
37
|
@split_step_token = nil
|
|
38
|
+
@split_step_prepared_descriptor = nil
|
|
39
|
+
@split_step_transaction_identity = nil
|
|
29
40
|
@split_step_execution_thread = nil
|
|
41
|
+
@split_step_active_execution_authorization = nil
|
|
42
|
+
@split_step_execution_result = nil
|
|
30
43
|
@split_step_advance_permit = false
|
|
44
|
+
clear_split_step_execution_verification!
|
|
45
|
+
clear_split_step_execution_authorization!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset_copied_dispatch_state!
|
|
49
|
+
@split_step_dispatch_token = nil
|
|
50
|
+
@split_step_dispatch_descriptor = nil
|
|
51
|
+
@split_step_attempted_dispatch_descriptor = nil
|
|
52
|
+
@split_step_dispatch_thread = nil
|
|
53
|
+
@split_step_pre_dispatch_payload = nil
|
|
54
|
+
@split_step_attempted_dispatch_payload = nil
|
|
31
55
|
@split_step_preparation_thread = nil
|
|
32
56
|
@split_step_persist_permit = false
|
|
33
57
|
end
|
|
34
|
-
private :initialize_copy
|
|
35
58
|
|
|
36
|
-
|
|
59
|
+
def detach_copied_runtime_state!(source)
|
|
60
|
+
source_messages = source.instance_variable_get(:@session_messages)
|
|
61
|
+
@split_step_mutex = Mutex.new
|
|
62
|
+
@persisted_keys_mutex = Mutex.new
|
|
63
|
+
@tool_results_mutex = Mutex.new
|
|
64
|
+
@usage_mutex = Mutex.new
|
|
65
|
+
detach_split_step_execution_state!
|
|
66
|
+
@session_messages = snapshot_value(source_messages || [])
|
|
67
|
+
end
|
|
37
68
|
|
|
38
69
|
def ensure_split_step_execution_allowed!
|
|
39
70
|
@split_step_mutex.synchronize do
|
|
@@ -91,24 +122,6 @@ module Smith
|
|
|
91
122
|
@split_step_execution_thread.equal?(Thread.current) &&
|
|
92
123
|
@split_step_advance_permit
|
|
93
124
|
end
|
|
94
|
-
|
|
95
|
-
def clear_split_step_boundary!
|
|
96
|
-
@split_step_phase = nil
|
|
97
|
-
@split_step_transition_name = nil
|
|
98
|
-
@split_step_transition = nil
|
|
99
|
-
@split_step_transition_signature = nil
|
|
100
|
-
@split_step_origin_state = nil
|
|
101
|
-
@split_step_token = nil
|
|
102
|
-
@split_step_persistence_key = nil
|
|
103
|
-
@split_step_adapter = nil
|
|
104
|
-
remove_instance_variable(:@split_step_persistence_ttl) if
|
|
105
|
-
instance_variable_defined?(:@split_step_persistence_ttl)
|
|
106
|
-
@split_step_preparation_payload = nil
|
|
107
|
-
@split_step_checkpoint_digest = nil
|
|
108
|
-
@split_step_checkpoint_version = nil
|
|
109
|
-
@split_step_preparation_thread = nil
|
|
110
|
-
@split_step_persist_permit = false
|
|
111
|
-
end
|
|
112
125
|
end
|
|
113
126
|
end
|
|
114
127
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module BoundaryReset
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def clear_split_step_boundary!
|
|
10
|
+
@split_step_phase = nil
|
|
11
|
+
@split_step_transition_name = nil
|
|
12
|
+
@split_step_transition = nil
|
|
13
|
+
@split_step_transition_signature = nil
|
|
14
|
+
@split_step_origin_state = nil
|
|
15
|
+
@split_step_token = nil
|
|
16
|
+
@split_step_active_execution_authorization = nil
|
|
17
|
+
@split_step_execution_result = nil
|
|
18
|
+
clear_split_step_descriptor_state!
|
|
19
|
+
@split_step_persistence_key = nil
|
|
20
|
+
@split_step_adapter = nil
|
|
21
|
+
remove_instance_variable(:@split_step_persistence_ttl) if
|
|
22
|
+
instance_variable_defined?(:@split_step_persistence_ttl)
|
|
23
|
+
@split_step_preparation_payload = nil
|
|
24
|
+
@split_step_checkpoint_digest = nil
|
|
25
|
+
@split_step_checkpoint_version = nil
|
|
26
|
+
@split_step_preparation_thread = nil
|
|
27
|
+
@split_step_persist_permit = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear_split_step_descriptor_state!
|
|
31
|
+
@split_step_prepared_descriptor = nil
|
|
32
|
+
@split_step_transaction_identity = nil
|
|
33
|
+
@split_step_preparation_digest = nil
|
|
34
|
+
@split_step_pending_descriptor = nil
|
|
35
|
+
@split_step_dispatch_started = false
|
|
36
|
+
@split_step_dispatch_token = nil
|
|
37
|
+
@split_step_dispatch_descriptor = nil
|
|
38
|
+
@split_step_attempted_dispatch_descriptor = nil
|
|
39
|
+
@split_step_dispatch_thread = nil
|
|
40
|
+
@split_step_pre_dispatch_payload = nil
|
|
41
|
+
@split_step_attempted_dispatch_payload = nil
|
|
42
|
+
clear_split_step_execution_verification!
|
|
43
|
+
clear_split_step_execution_authorization!
|
|
44
|
+
remove_instance_variable(:@split_step_definition_digest) if
|
|
45
|
+
instance_variable_defined?(:@split_step_definition_digest)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "dry-initializer"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
module SplitStepPersistence
|
|
10
|
+
class CanonicalPayloadDigest
|
|
11
|
+
extend Dry::Initializer
|
|
12
|
+
|
|
13
|
+
MAX_BYTES = 4 * 1024 * 1024
|
|
14
|
+
MAX_NODES = 100_000
|
|
15
|
+
|
|
16
|
+
param :payload
|
|
17
|
+
|
|
18
|
+
def self.call(payload) = new(payload).call
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
if payload.bytesize > MAX_BYTES
|
|
22
|
+
raise WorkflowError, "split-step preparation payload exceeds maximum bytes #{MAX_BYTES}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@nodes = 0
|
|
26
|
+
canonical = canonicalize(JSON.parse(payload))
|
|
27
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def canonicalize(value)
|
|
33
|
+
visit!
|
|
34
|
+
case value
|
|
35
|
+
when Hash then canonical_hash(value)
|
|
36
|
+
when Array then value.map { |item| canonicalize(item) }
|
|
37
|
+
when Float then value == value.to_i ? value.to_i : value
|
|
38
|
+
else value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def canonical_hash(value)
|
|
43
|
+
value.keys.sort.to_h { |key| [key, canonicalize(value.fetch(key))] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def visit!
|
|
47
|
+
@nodes += 1
|
|
48
|
+
return if @nodes <= MAX_NODES
|
|
49
|
+
|
|
50
|
+
raise WorkflowError, "split-step preparation payload exceeds maximum size #{MAX_NODES}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -91,11 +91,9 @@ module Smith
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def capture_split_step_payload!(payload, next_version, checkpoint_claimed:)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@split_step_preparation_payload = payload
|
|
98
|
-
end
|
|
94
|
+
return capture_split_step_preparation_payload!(payload, next_version) if @split_step_phase == :preparing
|
|
95
|
+
|
|
96
|
+
validate_split_step_marker!(payload, expected: true) if checkpoint_claimed
|
|
99
97
|
return payload unless checkpoint_claimed
|
|
100
98
|
|
|
101
99
|
checkpoint_payload = split_step_checkpoint_payload(payload)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../composite/contract"
|
|
4
|
+
require_relative "../composite_branch_execution_authorization"
|
|
5
|
+
require_relative "execution_binding_snapshot"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
module SplitStepPersistence
|
|
10
|
+
module CompositeBranchAuthorization
|
|
11
|
+
include Composite::Contract
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def authorize_prepared_composite_branch_execution!(execution:, input:)
|
|
16
|
+
authorization = nil
|
|
17
|
+
handed_off = false
|
|
18
|
+
authorization = CompositeBranchAuthorization
|
|
19
|
+
.instance_method(:interrupt_safe_composite_branch_authorization)
|
|
20
|
+
.bind_call(self, execution, input)
|
|
21
|
+
handed_off = true
|
|
22
|
+
authorization
|
|
23
|
+
ensure
|
|
24
|
+
unless handed_off
|
|
25
|
+
ExecutionLifecycle
|
|
26
|
+
.instance_method(:release_failed_execution_authorization!)
|
|
27
|
+
.bind_call(self, authorization)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with_composite_branch_authorization(execution, input)
|
|
32
|
+
Thread.handle_interrupt(Object => :never) do
|
|
33
|
+
authorization = interrupt_safe_composite_branch_authorization(execution, input)
|
|
34
|
+
begin
|
|
35
|
+
Thread.handle_interrupt(Object => :immediate) { yield(authorization) }
|
|
36
|
+
ensure
|
|
37
|
+
ExecutionLifecycle
|
|
38
|
+
.instance_method(:release_failed_execution_authorization!)
|
|
39
|
+
.bind_call(self, authorization)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def interrupt_safe_composite_branch_authorization(execution, input)
|
|
45
|
+
Thread.handle_interrupt(Object => :never) do
|
|
46
|
+
Composite::Contract
|
|
47
|
+
.instance_method(:validate_composite_branch_payload_types!)
|
|
48
|
+
.bind_call(self, execution, input)
|
|
49
|
+
verification_token = ExecutionAuthorization
|
|
50
|
+
.instance_method(:claim_framework_execution_verification!)
|
|
51
|
+
.bind_call(self)
|
|
52
|
+
begin
|
|
53
|
+
CompositeBranchAuthorization
|
|
54
|
+
.instance_method(:authorize_claimed_composite_branch_execution!)
|
|
55
|
+
.bind_call(self, verification_token, execution, input)
|
|
56
|
+
ensure
|
|
57
|
+
ExecutionAuthorization
|
|
58
|
+
.instance_method(:restore_framework_execution_verification!)
|
|
59
|
+
.bind_call(self, verification_token)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def authorize_claimed_composite_branch_execution!(verification_token, execution, input)
|
|
65
|
+
ExecutionVerification
|
|
66
|
+
.instance_method(:verify_claimed_split_step_execution!)
|
|
67
|
+
.bind_call(self, verification_token)
|
|
68
|
+
authorization = CompositeBranchAuthorization
|
|
69
|
+
.instance_method(:build_composite_branch_execution_authorization)
|
|
70
|
+
.bind_call(self, execution, input)
|
|
71
|
+
ExecutionAuthorization
|
|
72
|
+
.instance_method(:activate_split_step_execution_authorization!)
|
|
73
|
+
.bind_call(self, authorization, verification_token)
|
|
74
|
+
Composite::Contract
|
|
75
|
+
.instance_method(:validate_composite_branch_execution!)
|
|
76
|
+
.bind_call(self, authorization, execution, input)
|
|
77
|
+
authorization
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_composite_branch_execution_authorization(execution, input)
|
|
81
|
+
role = execution.kind == :parallel ? :agent : :fanout_agent
|
|
82
|
+
bindings = ExecutionBindingSnapshot.capture_agent(
|
|
83
|
+
@split_step_transition,
|
|
84
|
+
workflow_class: self.class,
|
|
85
|
+
name: execution.branch.agent,
|
|
86
|
+
role:
|
|
87
|
+
)
|
|
88
|
+
CompositeBranchExecutionAuthorization.new(
|
|
89
|
+
execution:,
|
|
90
|
+
input:,
|
|
91
|
+
prepared_step: @split_step_prepared_descriptor,
|
|
92
|
+
dispatch_claim: @split_step_dispatch_descriptor,
|
|
93
|
+
execution_bindings: bindings
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../composite/effects"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
module CompositeBranchEffects
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def composite_effect_offsets
|
|
12
|
+
usage = @usage_mutex.synchronize { @usage_entries.length }
|
|
13
|
+
tools = @tool_results_mutex.synchronize { @tool_results.length }
|
|
14
|
+
[usage, tools]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def capture_composite_branch_effects(usage_offset, tool_offset, branch)
|
|
18
|
+
usage = @usage_mutex.synchronize do
|
|
19
|
+
@usage_entries.drop(usage_offset).map { composite_usage_entry(_1, branch) }
|
|
20
|
+
end
|
|
21
|
+
tools = @tool_results_mutex.synchronize { @tool_results.drop(tool_offset) }
|
|
22
|
+
consumed = @ledger ? @ledger.consumed : {}
|
|
23
|
+
[Composite::Effects.new(usage_entries: usage, tool_results: tools, budget_consumed: consumed), nil]
|
|
24
|
+
rescue WorkflowError, ArgumentError => e
|
|
25
|
+
safe = Composite::Effects.new(usage_entries: usage || [], tool_results: [], budget_consumed: consumed || {})
|
|
26
|
+
[safe, e]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def composite_usage_entry(entry, branch)
|
|
30
|
+
attributes = entry.to_h
|
|
31
|
+
recorded_agent = attributes[:agent_name]
|
|
32
|
+
if recorded_agent && recorded_agent.to_s != branch.agent
|
|
33
|
+
raise WorkflowError, "composite usage entry does not match the executed branch agent"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
attributes.merge(agent_name: branch.agent)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../composite/contract"
|
|
4
|
+
require_relative "composite_branch_effects"
|
|
5
|
+
require_relative "composite_branch_outcome"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
module SplitStepPersistence
|
|
10
|
+
module CompositeBranchExecution
|
|
11
|
+
include Composite::Contract
|
|
12
|
+
include CompositeBranchEffects
|
|
13
|
+
include CompositeBranchOutcome
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def execute_authorized_composite_branch!(authorization, execution:, input:)
|
|
18
|
+
authorization = validate_composite_authorization!(authorization)
|
|
19
|
+
perform_authorized_prepared_step_execution!(authorization) do
|
|
20
|
+
validate_composite_branch_execution!(authorization, execution, input)
|
|
21
|
+
[nil, execute_composite_branch(authorization, execution, input)]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute_composite_branch(authorization, execution, input)
|
|
26
|
+
branch = execution.branch
|
|
27
|
+
previous_ledger = @ledger
|
|
28
|
+
previous_execution_namespace = @execution_namespace
|
|
29
|
+
@ledger = composite_branch_ledger(branch)
|
|
30
|
+
@execution_namespace = execution.execution_namespace
|
|
31
|
+
offsets = composite_effect_offsets
|
|
32
|
+
output, error = capture_composite_branch do
|
|
33
|
+
dispatch_composite_branch(authorization, execution, input)
|
|
34
|
+
end
|
|
35
|
+
effects, effect_error = capture_composite_branch_effects(*offsets, branch)
|
|
36
|
+
composite_branch_outcome(execution, output, error || effect_error, effects)
|
|
37
|
+
ensure
|
|
38
|
+
@ledger = previous_ledger
|
|
39
|
+
@execution_namespace = previous_execution_namespace
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def capture_composite_branch(&block)
|
|
43
|
+
output = within_raw_step_context { with_scoped_artifacts(&block) }
|
|
44
|
+
[output, nil]
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
[nil, e]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def dispatch_composite_branch(authorization, execution, input)
|
|
50
|
+
branch = execution.branch
|
|
51
|
+
transition = @split_step_transition
|
|
52
|
+
agent_role = execution.kind == :parallel ? :agent : :fanout_agent
|
|
53
|
+
agent_class = captured_agent(authorization, transition, branch.agent, agent_role)
|
|
54
|
+
apply_tool_guardrails(agent_class) if execution.kind == :parallel
|
|
55
|
+
environment = composite_branch_environment(execution, input, agent_class, transition)
|
|
56
|
+
execute_composite_branch_kind(execution, transition, agent_class, environment)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def execute_composite_branch_kind(execution, transition, agent_class, environment)
|
|
60
|
+
branch = execution.branch
|
|
61
|
+
signal = Parallel::CancellationSignal.new
|
|
62
|
+
return run_branch(transition, branch.ordinal, environment, @ledger, signal) if execution.kind == :parallel
|
|
63
|
+
|
|
64
|
+
branch_key, agent = fetch_composite_fanout_branch(transition, branch.key)
|
|
65
|
+
run_fanout_branch(branch_key, agent, agent_class, environment, signal)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def composite_branch_environment(execution, input, agent_class, transition)
|
|
69
|
+
branch = execution.branch
|
|
70
|
+
budget = branch.budget.transform_keys(&:to_sym)
|
|
71
|
+
branch_key = fetch_composite_fanout_branch(transition, branch.key).first unless execution.kind == :parallel
|
|
72
|
+
estimates = execution.kind == :parallel ? budget : { branch_key => budget }
|
|
73
|
+
BranchEnv.new(
|
|
74
|
+
prepared_input: input.agent_messages,
|
|
75
|
+
guardrail_sources: Tool.current_guardrails,
|
|
76
|
+
scoped_store: propagate_scoped_artifacts,
|
|
77
|
+
branch_estimates: estimates,
|
|
78
|
+
deadline: wall_clock_deadline,
|
|
79
|
+
agent_class: execution.kind == :parallel ? agent_class : nil
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def composite_branch_ledger(branch)
|
|
84
|
+
return if branch.budget.empty?
|
|
85
|
+
|
|
86
|
+
Budget::Ledger.new(limits: branch.budget.transform_keys(&:to_sym))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fetch_composite_fanout_branch(transition, branch_key)
|
|
90
|
+
Transition.instance_method(:fetch_fanout_branch!).bind_call(transition, branch_key)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|