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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -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/subclass_boundary.rb +21 -5
  63. data/lib/smith/workflow/split_step_persistence.rb +14 -0
  64. data/lib/smith/workflow/step_context.rb +46 -0
  65. data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
  66. data/lib/smith/workflow/transition.rb +16 -1
  67. data/lib/smith/workflow.rb +21 -1
  68. data/lib/smith.rb +3 -0
  69. 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
- 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
@@ -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