smith-agents 0.5.0 → 0.6.1

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +83 -0
  4. data/lib/smith/agent.rb +13 -0
  5. data/lib/smith/errors.rb +4 -0
  6. data/lib/smith/version.rb +2 -2
  7. data/lib/smith/workflow/composite/branch.rb +93 -0
  8. data/lib/smith/workflow/composite/branch_budget_contract.rb +41 -0
  9. data/lib/smith/workflow/composite/branch_contract.rb +102 -0
  10. data/lib/smith/workflow/composite/branch_execution.rb +124 -0
  11. data/lib/smith/workflow/composite/branch_failure.rb +91 -0
  12. data/lib/smith/workflow/composite/branch_outcome.rb +108 -0
  13. data/lib/smith/workflow/composite/budget_allocator.rb +78 -0
  14. data/lib/smith/workflow/composite/budget_math.rb +51 -0
  15. data/lib/smith/workflow/composite/contract.rb +100 -0
  16. data/lib/smith/workflow/composite/effects.rb +140 -0
  17. data/lib/smith/workflow/composite/effects_application.rb +36 -0
  18. data/lib/smith/workflow/composite/effects_baseline.rb +36 -0
  19. data/lib/smith/workflow/composite/effects_preflight.rb +102 -0
  20. data/lib/smith/workflow/composite/encoded_value_budget.rb +36 -0
  21. data/lib/smith/workflow/composite/enums.rb +27 -0
  22. data/lib/smith/workflow/composite/error.rb +54 -0
  23. data/lib/smith/workflow/composite/error_evidence.rb +61 -0
  24. data/lib/smith/workflow/composite/execution_contract.rb +73 -0
  25. data/lib/smith/workflow/composite/fanout_branch_contract.rb +61 -0
  26. data/lib/smith/workflow/composite/input.rb +53 -0
  27. data/lib/smith/workflow/composite/outcome_accumulator.rb +131 -0
  28. data/lib/smith/workflow/composite/outcome_set.rb +23 -0
  29. data/lib/smith/workflow/composite/payload.rb +128 -0
  30. data/lib/smith/workflow/composite/payload_digest.rb +28 -0
  31. data/lib/smith/workflow/composite/plan.rb +130 -0
  32. data/lib/smith/workflow/composite/plan_integrity.rb +52 -0
  33. data/lib/smith/workflow/composite/planner.rb +53 -0
  34. data/lib/smith/workflow/composite/preparation.rb +27 -0
  35. data/lib/smith/workflow/composite/reducer.rb +133 -0
  36. data/lib/smith/workflow/composite/reduction.rb +31 -0
  37. data/lib/smith/workflow/composite/value_budget.rb +90 -0
  38. data/lib/smith/workflow/composite_branch_execution_authorization.rb +52 -0
  39. data/lib/smith/workflow/deadline_enforcement.rb +22 -5
  40. data/lib/smith/workflow/execution.rb +16 -16
  41. data/lib/smith/workflow/fanout_execution.rb +19 -18
  42. data/lib/smith/workflow/message_value_normalizer.rb +11 -10
  43. data/lib/smith/workflow/parallel_execution.rb +41 -13
  44. data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
  45. data/lib/smith/workflow/prepared_step_execution_authorization.rb +14 -29
  46. data/lib/smith/workflow/prepared_step_execution_scope.rb +81 -15
  47. data/lib/smith/workflow/process_local.rb +33 -0
  48. data/lib/smith/workflow/split_step_persistence/composite_branch_authorization.rb +99 -0
  49. data/lib/smith/workflow/split_step_persistence/composite_branch_effects.rb +41 -0
  50. data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +95 -0
  51. data/lib/smith/workflow/split_step_persistence/composite_branch_outcome.rb +36 -0
  52. data/lib/smith/workflow/split_step_persistence/composite_execution.rb +39 -0
  53. data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +57 -0
  54. data/lib/smith/workflow/split_step_persistence/composite_reduction_execution.rb +122 -0
  55. data/lib/smith/workflow/split_step_persistence/execution.rb +7 -53
  56. data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +41 -21
  57. data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
  58. data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +5 -0
  59. data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +17 -3
  60. data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
  61. data/lib/smith/workflow/split_step_persistence/execution_verification.rb +17 -5
  62. data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +8 -0
  63. data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +21 -5
  64. data/lib/smith/workflow/split_step_persistence.rb +14 -0
  65. data/lib/smith/workflow/step_context.rb +46 -0
  66. data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
  67. data/lib/smith/workflow/transition.rb +16 -1
  68. data/lib/smith/workflow.rb +21 -1
  69. data/lib/smith.rb +3 -0
  70. metadata +46 -1
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "prepared_step_execution_authorization"
4
+ require_relative "composite/branch_execution"
5
+ require_relative "composite/input"
6
+
7
+ module Smith
8
+ class Workflow
9
+ class CompositeBranchExecutionAuthorization < PreparedStepExecutionAuthorization
10
+ attr_reader :execution_digest, :plan_digest, :input_digest, :branch_digest,
11
+ :branch_ordinal, :execution_namespace
12
+
13
+ def initialize(execution:, input:, **attributes)
14
+ validate_payloads!(execution, input)
15
+ @execution_digest = execution.digest.dup.freeze
16
+ @plan_digest = execution.plan_digest.dup.freeze
17
+ @input_digest = input.digest.dup.freeze
18
+ assign_branch_identity(execution.branch)
19
+ @execution_namespace = execution.execution_namespace.dup.freeze
20
+ super(**attributes)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_payloads!(execution, input)
26
+ return if execution.is_a?(Composite::BranchExecution) && input.is_a?(Composite::Input)
27
+
28
+ raise ArgumentError, "composite branch authorization requires typed execution and input"
29
+ end
30
+
31
+ def assign_branch_identity(branch)
32
+ @branch_digest = branch.digest.dup.freeze
33
+ @branch_ordinal = branch.ordinal
34
+ end
35
+
36
+ def verify_composite_branch!(execution, input)
37
+ validate_payloads!(execution, input)
38
+ expected = [
39
+ execution_digest, plan_digest, branch_digest, branch_ordinal,
40
+ execution_namespace, input_digest
41
+ ]
42
+ actual = [
43
+ execution.digest, execution.plan_digest, execution.branch.digest,
44
+ execution.branch.ordinal, execution.execution_namespace, input.digest
45
+ ]
46
+ return if actual == expected
47
+
48
+ raise WorkflowError, "composite branch authorization does not match execution"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "time"
4
4
 
5
+ require_relative "thread_context_snapshot"
6
+
5
7
  module Smith
6
8
  class Workflow
7
9
  module DeadlineEnforcement
@@ -19,17 +21,32 @@ module Smith
19
21
  [wall_clock_deadline, call_dl].compact.min
20
22
  end
21
23
 
22
- def with_agent_context(agent_class)
24
+ def with_agent_context(agent_class, &block)
23
25
  saved_deadline = Tool.current_deadline
24
26
  saved_call_ledger = Thread.current[:smith_call_ledger]
27
+ snapshot = ThreadContextSnapshot.new(
28
+ tool_attributes: %i[current_deadline current_tool_call_allowance],
29
+ thread_keys: %i[smith_call_deadline smith_call_ledger],
30
+ scoped_artifacts: false
31
+ )
32
+ snapshot.around do
33
+ apply_agent_context(agent_class)
34
+ Thread.handle_interrupt(Object => :immediate, &block)
35
+ ensure
36
+ restore_agent_context(saved_deadline, saved_call_ledger)
37
+ end
38
+ end
39
+
40
+ def apply_agent_context(agent_class)
25
41
  apply_agent_deadline(agent_class)
26
42
  narrow_tool_deadline!
27
43
  apply_agent_tool_calls(agent_class)
28
44
  apply_agent_call_ledger(agent_class)
29
- yield
30
- ensure
31
- Tool.current_deadline = saved_deadline
32
- Thread.current[:smith_call_ledger] = saved_call_ledger
45
+ end
46
+
47
+ def restore_agent_context(deadline, call_ledger)
48
+ Tool.current_deadline = deadline
49
+ Thread.current[:smith_call_ledger] = call_ledger
33
50
  clear_agent_deadline
34
51
  clear_agent_tool_calls
35
52
  end
@@ -2,13 +2,17 @@
2
2
 
3
3
  require_relative "agent_result"
4
4
  require_relative "execution_binding_resolution"
5
+ require_relative "prepared_branch_execution"
5
6
  require_relative "step_completion"
7
+ require_relative "step_context"
6
8
 
7
9
  module Smith
8
10
  class Workflow
9
11
  module Execution
10
12
  include ExecutionBindingResolution
13
+ include PreparedBranchExecution
11
14
  include StepCompletion
15
+ include StepContext
12
16
  include Agent::Lifecycle
13
17
  include NestedExecution
14
18
  include EvaluatorOptimizer
@@ -21,14 +25,12 @@ module Smith
21
25
  private
22
26
 
23
27
  def execute_step(transition)
24
- setup_step_context
28
+ with_step_context(transition) { execute_step_body(transition) }
29
+ end
30
+
31
+ def execute_step_body(transition)
25
32
  output = with_scoped_artifacts { run_with_retry_policy(transition) }
26
33
  StepCompletion.instance_method(:complete_step).bind_call(self, transition, output)
27
- rescue StandardError => e
28
- @outcome = nil
29
- GuardrailIntegration.instance_method(:handle_step_failure).bind_call(self, transition, e)
30
- ensure
31
- teardown_step_context
32
34
  end
33
35
 
34
36
  def setup_step_context
@@ -37,16 +39,16 @@ module Smith
37
39
  Tool.current_tool_result_collector = tool_result_collector
38
40
  end
39
41
 
40
- def teardown_step_context
41
- Tool.current_guardrails = nil
42
- Tool.current_deadline = nil
43
- Tool.current_ledger = nil
44
- Tool.current_tool_result_collector = nil
45
- Smith.scoped_artifacts = nil
46
- end
47
-
48
42
  def run_guarded_step(transition)
43
+ return apply_composite_reduction!(transition) if @composite_reduction
44
+
49
45
  @resolved_parallel_branch_count = preflight_branch_count(transition)
46
+ run_standard_guarded_step(transition)
47
+ ensure
48
+ @resolved_parallel_branch_count = nil
49
+ end
50
+
51
+ def run_standard_guarded_step(transition)
50
52
  return dispatch_step(transition) if transition.deterministic?
51
53
  return run_guarded_fanout_step(transition) if transition.fanout?
52
54
 
@@ -62,8 +64,6 @@ module Smith
62
64
  validate_data_volume!(output, agent_class)
63
65
  run_output_guardrails(output, agent_class)
64
66
  resolve_router_output(transition, output)
65
- ensure
66
- @resolved_parallel_branch_count = nil
67
67
  end
68
68
 
69
69
  def preflight_branch_count(transition)
@@ -35,29 +35,32 @@ module Smith
35
35
  )
36
36
 
37
37
  branch_calls = branches.map do |branch_key, agent_name|
38
- proc do |signal|
39
- run_fanout_branch(branch_key, agent_name, branch_agent_classes.fetch(branch_key), env, signal)
40
- end
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
- setup_fanout_branch_context(env, @ledger, agent_class)
48
-
49
- with_agent_context(agent_class) do
50
- branch_ledger = effective_call_ledger
51
- reserved = reserve_fanout_branch_call(branch_ledger, env.branch_estimates[branch_key], agent_class)
52
- begin
53
- result = guarded_fanout_branch_call(agent_class, env, signal)
54
- finalize_named_branch(branch_key, agent_name, result, branch_ledger, reserved).tap { reserved = nil }
55
- ensure
56
- settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
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 do |agent_class|
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!("session message contains unsupported value #{item.class}")
54
+ reject!("#{label} contains unsupported value #{item.class}")
54
55
  end
55
56
  end
56
57
 
57
58
  def copy_container(source)
58
- reject!("session message contains a cyclic value") if @active.key?(source)
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!("session message batch exceeds maximum size #{MAX_NODES}") if pairs.length == remaining_nodes
81
- reject!("session message Hash keys must be strings or symbols") unless key.is_a?(String) || key.is_a?(Symbol)
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!("session message contains duplicate canonical Hash keys")
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!("session message batch exceeds maximum bytes #{MAX_BYTES}") if @bytes > MAX_BYTES
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!("session message integers must fit a signed 64-bit value")
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!("session message contains a non-finite Float")
117
+ reject!("#{label} contains a non-finite Float")
117
118
  end
118
119
 
119
120
  def visit!(depth)
120
- reject!("session message batch exceeds maximum depth #{MAX_DEPTH}") if depth > MAX_DEPTH
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!("session message batch exceeds maximum size #{MAX_NODES}")
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
- proc { |signal| run_branch(transition, i, env, ledger, signal) }
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
- setup_branch_context(env, ledger)
32
- Thread.current[:smith_parallel_agent_binding] = ParallelAgentBinding.new(self, transition, env.agent_class)
33
- with_agent_context(env.agent_class) do
34
- branch_ledger = effective_call_ledger
35
- reserved = reserve_branch_call(branch_ledger, env, ledger)
36
- begin
37
- result = guarded_branch_call(transition, env, signal)
38
- finalize_branch(transition, index, result, branch_ledger, reserved).tap { reserved = nil }
39
- ensure
40
- settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
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
- def init_with(_coder)
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 to_json(*)
82
- raise TypeError, "prepared-step execution authorizations cannot be serialized"
66
+ def within_branch_execution!(&)
67
+ ensure_current_process!
68
+ @execution_scope.within_branch(&)
83
69
  end
84
70
 
85
- private
86
-
87
- def activate_execution!(thread)
71
+ def activate_execution!
88
72
  ensure_current_process!
89
- @execution_scope.activate!(thread)
73
+ @execution_scope.activate!(Thread.current, Fiber.current)
90
74
  end
91
75
 
92
- def close_execution!(thread = nil)
93
- @execution_scope.close!(thread)
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? && @execution_scope.binding_accessible_for?(Thread.current)
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
- @mutex.synchronize do
16
- raise WorkflowError, "prepared-step execution scope is no longer available" unless @phase == :issued
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
- @phase = :active
19
- @thread = thread
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
- @mutex.synchronize do
25
- if @phase == :active && thread && !@thread.equal?(thread)
26
- raise WorkflowError, "prepared-step execution scope belongs to another thread"
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
- @phase = :closed
30
- @thread = nil
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 { @phase == :active && @thread.equal?(thread) }
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 || (@phase == :active && @thread.equal?(thread))
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