smith-agents 0.5.0 → 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 +63 -0
- data/README.md +83 -0
- data/lib/smith/agent.rb +13 -0
- data/lib/smith/errors.rb +4 -0
- data/lib/smith/version.rb +2 -2
- 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/execution.rb +16 -16
- data/lib/smith/workflow/fanout_execution.rb +19 -18
- data/lib/smith/workflow/message_value_normalizer.rb +11 -10
- data/lib/smith/workflow/parallel_execution.rb +41 -13
- data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
- data/lib/smith/workflow/prepared_step_execution_authorization.rb +14 -29
- data/lib/smith/workflow/prepared_step_execution_scope.rb +81 -15
- data/lib/smith/workflow/process_local.rb +33 -0
- 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/execution.rb +7 -53
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +41 -21
- data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +5 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +17 -3
- data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +17 -5
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +21 -5
- data/lib/smith/workflow/split_step_persistence.rb +14 -0
- data/lib/smith/workflow/step_context.rb +46 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
- data/lib/smith/workflow/transition.rb +16 -1
- data/lib/smith/workflow.rb +21 -1
- data/lib/smith.rb +3 -0
- metadata +46 -1
|
@@ -35,29 +35,32 @@ module Smith
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
branch_calls = branches.map do |branch_key, agent_name|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
PreparedBranchExecution.instance_method(:prepared_branch).bind_call(
|
|
39
|
+
self,
|
|
40
|
+
FanoutExecution.instance_method(:run_fanout_branch),
|
|
41
|
+
branch_key,
|
|
42
|
+
agent_name,
|
|
43
|
+
branch_agent_classes.fetch(branch_key),
|
|
44
|
+
env
|
|
45
|
+
)
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
Parallel.execute(branches: branch_calls)
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def run_fanout_branch(branch_key, agent_name, agent_class, env, signal)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
with_branch_context(env, @ledger, agent_class:) do
|
|
53
|
+
with_agent_context(agent_class) do
|
|
54
|
+
branch_ledger = effective_call_ledger
|
|
55
|
+
reserved = reserve_fanout_branch_call(branch_ledger, env.branch_estimates[branch_key], agent_class)
|
|
56
|
+
begin
|
|
57
|
+
result = guarded_fanout_branch_call(agent_class, env, signal)
|
|
58
|
+
finalize_named_branch(branch_key, agent_name, result, branch_ledger, reserved).tap { reserved = nil }
|
|
59
|
+
ensure
|
|
60
|
+
settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
|
|
61
|
+
end
|
|
57
62
|
end
|
|
58
63
|
end
|
|
59
|
-
ensure
|
|
60
|
-
teardown_branch_context(env)
|
|
61
64
|
end
|
|
62
65
|
|
|
63
66
|
def guarded_fanout_branch_call(agent_class, env, signal)
|
|
@@ -89,9 +92,7 @@ module Smith
|
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def run_fanout_agent_input_guardrails(branch_agent_classes)
|
|
92
|
-
branch_agent_classes.each_value
|
|
93
|
-
run_agent_input_guardrails(agent_class)
|
|
94
|
-
end
|
|
95
|
+
branch_agent_classes.each_value { |agent_class| run_agent_input_guardrails(agent_class) }
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
def fanout_branch_estimates(branches, branch_agent_classes)
|
|
@@ -19,6 +19,7 @@ module Smith
|
|
|
19
19
|
extend Dry::Initializer
|
|
20
20
|
|
|
21
21
|
param :value
|
|
22
|
+
option :label, default: proc { "session message" }
|
|
22
23
|
|
|
23
24
|
def call
|
|
24
25
|
@active = {}.compare_by_identity
|
|
@@ -50,12 +51,12 @@ module Smith
|
|
|
50
51
|
when Float then copy_float(item)
|
|
51
52
|
when true, false, nil then item
|
|
52
53
|
else
|
|
53
|
-
reject!("
|
|
54
|
+
reject!("#{label} contains unsupported value #{item.class}")
|
|
54
55
|
end
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def copy_container(source)
|
|
58
|
-
reject!("
|
|
59
|
+
reject!("#{label} contains a cyclic value") if @active.key?(source)
|
|
59
60
|
|
|
60
61
|
@active[source] = true
|
|
61
62
|
yield
|
|
@@ -77,8 +78,8 @@ module Smith
|
|
|
77
78
|
pairs = []
|
|
78
79
|
remaining_nodes = MAX_NODES - @nodes
|
|
79
80
|
HASH_EACH_PAIR.bind_call(source) do |key, nested|
|
|
80
|
-
reject!("
|
|
81
|
-
reject!("
|
|
81
|
+
reject!("#{label} exceeds maximum size #{MAX_NODES}") if pairs.length == remaining_nodes
|
|
82
|
+
reject!("#{label} Hash keys must be strings or symbols") unless key.is_a?(String) || key.is_a?(Symbol)
|
|
82
83
|
|
|
83
84
|
pairs << [copy_string(key.to_s), nested]
|
|
84
85
|
end
|
|
@@ -88,7 +89,7 @@ module Smith
|
|
|
88
89
|
def validate_unique_keys!(pairs)
|
|
89
90
|
return unless pairs.each_cons(2).any? { |left, right| left.first == right.first }
|
|
90
91
|
|
|
91
|
-
reject!("
|
|
92
|
+
reject!("#{label} contains duplicate canonical Hash keys")
|
|
92
93
|
end
|
|
93
94
|
|
|
94
95
|
def copy_array(source, depth:)
|
|
@@ -99,7 +100,7 @@ module Smith
|
|
|
99
100
|
|
|
100
101
|
def copy_string(string)
|
|
101
102
|
@bytes += StringSnapshot.bytesize(string)
|
|
102
|
-
reject!("
|
|
103
|
+
reject!("#{label} exceeds maximum bytes #{MAX_BYTES}") if @bytes > MAX_BYTES
|
|
103
104
|
|
|
104
105
|
StringSnapshot.copy(string, freeze: true)
|
|
105
106
|
end
|
|
@@ -107,22 +108,22 @@ module Smith
|
|
|
107
108
|
def copy_integer(integer)
|
|
108
109
|
return integer if INTEGER_RANGE.cover?(integer)
|
|
109
110
|
|
|
110
|
-
reject!("
|
|
111
|
+
reject!("#{label} integers must fit a signed 64-bit value")
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
def copy_float(float)
|
|
114
115
|
return float if float.finite?
|
|
115
116
|
|
|
116
|
-
reject!("
|
|
117
|
+
reject!("#{label} contains a non-finite Float")
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
def visit!(depth)
|
|
120
|
-
reject!("
|
|
121
|
+
reject!("#{label} exceeds maximum depth #{MAX_DEPTH}") if depth > MAX_DEPTH
|
|
121
122
|
|
|
122
123
|
@nodes += 1
|
|
123
124
|
return if @nodes <= MAX_NODES
|
|
124
125
|
|
|
125
|
-
reject!("
|
|
126
|
+
reject!("#{label} exceeds maximum size #{MAX_NODES}")
|
|
126
127
|
end
|
|
127
128
|
|
|
128
129
|
def reject!(message)
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "branch_env"
|
|
4
4
|
require_relative "parallel_agent_binding"
|
|
5
|
+
require_relative "thread_context_snapshot"
|
|
5
6
|
|
|
6
7
|
module Smith
|
|
7
8
|
class Workflow
|
|
8
9
|
module ParallelExecution
|
|
10
|
+
NO_PARALLEL_BINDING = Object.new.freeze
|
|
11
|
+
private_constant :NO_PARALLEL_BINDING
|
|
12
|
+
|
|
9
13
|
private
|
|
10
14
|
|
|
11
15
|
def execute_parallel_step(transition, prepared_input: nil)
|
|
@@ -22,26 +26,27 @@ module Smith
|
|
|
22
26
|
)
|
|
23
27
|
ledger = @ledger
|
|
24
28
|
branches = Array.new(count) do |i|
|
|
25
|
-
|
|
29
|
+
PreparedBranchExecution
|
|
30
|
+
.instance_method(:prepared_branch)
|
|
31
|
+
.bind_call(self, ParallelExecution.instance_method(:run_branch), transition, i, env, ledger)
|
|
26
32
|
end
|
|
27
33
|
Parallel.execute(branches: branches)
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
def run_branch(transition, index, env, ledger, signal)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
binding = ParallelAgentBinding.new(self, transition, env.agent_class)
|
|
38
|
+
with_branch_context(env, ledger, parallel_agent_binding: binding) do
|
|
39
|
+
with_agent_context(env.agent_class) do
|
|
40
|
+
branch_ledger = effective_call_ledger
|
|
41
|
+
reserved = reserve_branch_call(branch_ledger, env, ledger)
|
|
42
|
+
begin
|
|
43
|
+
result = guarded_branch_call(transition, env, signal)
|
|
44
|
+
finalize_branch(transition, index, result, branch_ledger, reserved).tap { reserved = nil }
|
|
45
|
+
ensure
|
|
46
|
+
settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
|
|
47
|
+
end
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
|
-
ensure
|
|
44
|
-
teardown_branch_context(env)
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
def reserve_branch_call(branch_ledger, env, workflow_ledger)
|
|
@@ -50,6 +55,29 @@ module Smith
|
|
|
50
55
|
reserve_serial_budget(branch_ledger) if branch_ledger
|
|
51
56
|
end
|
|
52
57
|
|
|
58
|
+
def with_branch_context(
|
|
59
|
+
env,
|
|
60
|
+
ledger,
|
|
61
|
+
parallel_agent_binding: NO_PARALLEL_BINDING,
|
|
62
|
+
agent_class: nil,
|
|
63
|
+
&block
|
|
64
|
+
)
|
|
65
|
+
snapshot = ThreadContextSnapshot.new
|
|
66
|
+
snapshot.around do
|
|
67
|
+
if agent_class
|
|
68
|
+
setup_fanout_branch_context(env, ledger, agent_class)
|
|
69
|
+
else
|
|
70
|
+
setup_branch_context(env, ledger)
|
|
71
|
+
end
|
|
72
|
+
unless parallel_agent_binding.equal?(NO_PARALLEL_BINDING)
|
|
73
|
+
Thread.current[:smith_parallel_agent_binding] = parallel_agent_binding
|
|
74
|
+
end
|
|
75
|
+
Thread.handle_interrupt(Object => :immediate, &block)
|
|
76
|
+
ensure
|
|
77
|
+
teardown_branch_context(env)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
53
81
|
def setup_branch_context(env, ledger)
|
|
54
82
|
env.setup_thread
|
|
55
83
|
Tool.current_ledger = ledger
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module PreparedBranchExecution
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def prepared_branch(implementation, *arguments)
|
|
9
|
+
unless @split_step_active_execution_authorization
|
|
10
|
+
return proc { |signal| __send__(implementation.name, *arguments, signal) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
proc do |signal|
|
|
14
|
+
run = proc { implementation.bind_call(self, *arguments, signal) }
|
|
15
|
+
PreparedBranchExecution.instance_method(:within_prepared_branch_execution).bind_call(self, &run)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def within_prepared_branch_execution(&)
|
|
20
|
+
authorization = @split_step_active_execution_authorization
|
|
21
|
+
return yield unless authorization
|
|
22
|
+
|
|
23
|
+
PreparedStepExecutionAuthorization
|
|
24
|
+
.instance_method(:within_branch_execution!)
|
|
25
|
+
.bind_call(authorization, &)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_constant :PreparedBranchExecution
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
require_relative "prepared_step"
|
|
4
4
|
require_relative "prepared_step_dispatch"
|
|
5
5
|
require_relative "prepared_step_execution_scope"
|
|
6
|
+
require_relative "process_local"
|
|
6
7
|
require_relative "split_step_persistence/execution_binding_snapshot"
|
|
7
8
|
require_relative "../errors"
|
|
8
9
|
|
|
9
10
|
module Smith
|
|
10
11
|
class Workflow
|
|
11
12
|
class PreparedStepExecutionAuthorization
|
|
13
|
+
include ProcessLocal
|
|
14
|
+
|
|
12
15
|
attr_reader :prepared_step, :dispatch_claim
|
|
13
16
|
|
|
14
17
|
def initialize(prepared_step:, dispatch_claim:, execution_bindings:)
|
|
@@ -55,42 +58,23 @@ module Smith
|
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def active_in_current_execution?
|
|
58
|
-
issued_in_current_process? && @execution_scope.active_for?(Thread.current)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def initialize_copy(_source)
|
|
62
|
-
raise TypeError, "prepared-step execution authorizations cannot be copied"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def _dump(_depth)
|
|
66
|
-
raise TypeError, "prepared-step execution authorizations cannot be serialized"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def encode_with(_coder)
|
|
70
|
-
raise TypeError, "prepared-step execution authorizations cannot be serialized"
|
|
61
|
+
issued_in_current_process? && @execution_scope.active_for?(Thread.current, Fiber.current)
|
|
71
62
|
end
|
|
72
63
|
|
|
73
|
-
|
|
74
|
-
raise TypeError, "prepared-step execution authorizations cannot be deserialized"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def as_json(*)
|
|
78
|
-
raise TypeError, "prepared-step execution authorizations cannot be serialized"
|
|
79
|
-
end
|
|
64
|
+
private
|
|
80
65
|
|
|
81
|
-
def
|
|
82
|
-
|
|
66
|
+
def within_branch_execution!(&)
|
|
67
|
+
ensure_current_process!
|
|
68
|
+
@execution_scope.within_branch(&)
|
|
83
69
|
end
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def activate_execution!(thread)
|
|
71
|
+
def activate_execution!
|
|
88
72
|
ensure_current_process!
|
|
89
|
-
@execution_scope.activate!(
|
|
73
|
+
@execution_scope.activate!(Thread.current, Fiber.current)
|
|
90
74
|
end
|
|
91
75
|
|
|
92
|
-
def close_execution!
|
|
93
|
-
@execution_scope.close!(
|
|
76
|
+
def close_execution!
|
|
77
|
+
@execution_scope.close!(Thread.current, Fiber.current)
|
|
94
78
|
end
|
|
95
79
|
|
|
96
80
|
def ensure_active_execution!
|
|
@@ -100,7 +84,8 @@ module Smith
|
|
|
100
84
|
end
|
|
101
85
|
|
|
102
86
|
def ensure_binding_access!
|
|
103
|
-
accessible = issued_in_current_process? &&
|
|
87
|
+
accessible = issued_in_current_process? &&
|
|
88
|
+
@execution_scope.binding_accessible_for?(Thread.current, Fiber.current)
|
|
104
89
|
return if accessible
|
|
105
90
|
|
|
106
91
|
raise WorkflowError, "prepared-step execution authorization is outside its binding access scope"
|
|
@@ -1,45 +1,111 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../errors"
|
|
4
|
+
require_relative "process_local"
|
|
4
5
|
|
|
5
6
|
module Smith
|
|
6
7
|
class Workflow
|
|
7
8
|
class PreparedStepExecutionScope
|
|
9
|
+
include ProcessLocal
|
|
10
|
+
|
|
8
11
|
def initialize
|
|
9
12
|
@mutex = Mutex.new
|
|
10
13
|
@phase = :issued
|
|
11
14
|
@thread = nil
|
|
15
|
+
@fiber = nil
|
|
16
|
+
@branch_fibers = {}.compare_by_identity
|
|
12
17
|
end
|
|
13
18
|
|
|
14
|
-
def activate!(thread)
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
@phase = :active
|
|
25
|
+
@thread = thread
|
|
26
|
+
@fiber = fiber
|
|
27
|
+
end
|
|
20
28
|
end
|
|
21
29
|
end
|
|
22
30
|
|
|
23
|
-
def close!(thread = nil)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
27
42
|
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
31
56
|
end
|
|
32
57
|
end
|
|
33
58
|
|
|
34
|
-
def active_for?(thread)
|
|
35
|
-
@mutex.synchronize
|
|
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
|
|
36
63
|
end
|
|
37
64
|
|
|
38
|
-
def binding_accessible_for?(thread)
|
|
65
|
+
def binding_accessible_for?(thread, fiber = Fiber.current)
|
|
39
66
|
@mutex.synchronize do
|
|
40
|
-
@phase == :issued ||
|
|
67
|
+
@phase == :issued ||
|
|
68
|
+
(@phase == :active && (owner?(@thread, @fiber, thread, fiber) || branch_owner?(thread, fiber)))
|
|
41
69
|
end
|
|
42
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
|
|
43
109
|
end
|
|
44
110
|
end
|
|
45
111
|
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
|
|
@@ -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
|