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.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +191 -4
  4. data/docs/CONFIGURATION.md +4 -1
  5. data/docs/PATTERNS.md +7 -0
  6. data/docs/PERSISTENCE.md +347 -25
  7. data/lib/smith/agent/lifecycle.rb +19 -10
  8. data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
  9. data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
  10. data/lib/smith/agent/registry.rb +8 -0
  11. data/lib/smith/agent.rb +37 -2
  12. data/lib/smith/budget/amount_contract.rb +67 -0
  13. data/lib/smith/budget/amount_representation.rb +43 -0
  14. data/lib/smith/budget/decimal_context.rb +18 -0
  15. data/lib/smith/budget/ledger.rb +110 -18
  16. data/lib/smith/budget/ledger_publication.rb +60 -0
  17. data/lib/smith/budget/ledger_state.rb +21 -0
  18. data/lib/smith/budget/ledger_state_transition.rb +72 -0
  19. data/lib/smith/budget/public_state_validator.rb +25 -0
  20. data/lib/smith/budget/reservation.rb +20 -0
  21. data/lib/smith/budget/reservation_contract.rb +34 -0
  22. data/lib/smith/context/observation_masking.rb +3 -1
  23. data/lib/smith/context/state_injection.rb +1 -1
  24. data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -4
  25. data/lib/smith/error.rb +5 -0
  26. data/lib/smith/errors.rb +10 -0
  27. data/lib/smith/exponential_backoff.rb +128 -0
  28. data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
  29. data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
  30. data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
  31. data/lib/smith/persistence_adapters/active_record_store.rb +7 -4
  32. data/lib/smith/persistence_adapters/cache_store.rb +1 -0
  33. data/lib/smith/persistence_adapters/memory.rb +16 -1
  34. data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
  35. data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
  36. data/lib/smith/persistence_adapters/redis_store.rb +28 -11
  37. data/lib/smith/persistence_adapters/retry.rb +22 -10
  38. data/lib/smith/persistence_adapters.rb +21 -18
  39. data/lib/smith/persistence_payload_conflict.rb +12 -0
  40. data/lib/smith/tool/budget_enforcement.rb +2 -2
  41. data/lib/smith/types.rb +1 -0
  42. data/lib/smith/version.rb +2 -1
  43. data/lib/smith/workflow/branch_env.rb +1 -1
  44. data/lib/smith/workflow/budget_integration.rb +21 -11
  45. data/lib/smith/workflow/composite/branch.rb +93 -0
  46. data/lib/smith/workflow/composite/branch_budget_contract.rb +41 -0
  47. data/lib/smith/workflow/composite/branch_contract.rb +102 -0
  48. data/lib/smith/workflow/composite/branch_execution.rb +124 -0
  49. data/lib/smith/workflow/composite/branch_failure.rb +91 -0
  50. data/lib/smith/workflow/composite/branch_outcome.rb +108 -0
  51. data/lib/smith/workflow/composite/budget_allocator.rb +78 -0
  52. data/lib/smith/workflow/composite/budget_math.rb +51 -0
  53. data/lib/smith/workflow/composite/contract.rb +100 -0
  54. data/lib/smith/workflow/composite/effects.rb +140 -0
  55. data/lib/smith/workflow/composite/effects_application.rb +36 -0
  56. data/lib/smith/workflow/composite/effects_baseline.rb +36 -0
  57. data/lib/smith/workflow/composite/effects_preflight.rb +102 -0
  58. data/lib/smith/workflow/composite/encoded_value_budget.rb +36 -0
  59. data/lib/smith/workflow/composite/enums.rb +27 -0
  60. data/lib/smith/workflow/composite/error.rb +54 -0
  61. data/lib/smith/workflow/composite/error_evidence.rb +61 -0
  62. data/lib/smith/workflow/composite/execution_contract.rb +73 -0
  63. data/lib/smith/workflow/composite/fanout_branch_contract.rb +61 -0
  64. data/lib/smith/workflow/composite/input.rb +53 -0
  65. data/lib/smith/workflow/composite/outcome_accumulator.rb +131 -0
  66. data/lib/smith/workflow/composite/outcome_set.rb +23 -0
  67. data/lib/smith/workflow/composite/payload.rb +128 -0
  68. data/lib/smith/workflow/composite/payload_digest.rb +28 -0
  69. data/lib/smith/workflow/composite/plan.rb +130 -0
  70. data/lib/smith/workflow/composite/plan_integrity.rb +52 -0
  71. data/lib/smith/workflow/composite/planner.rb +53 -0
  72. data/lib/smith/workflow/composite/preparation.rb +27 -0
  73. data/lib/smith/workflow/composite/reducer.rb +133 -0
  74. data/lib/smith/workflow/composite/reduction.rb +31 -0
  75. data/lib/smith/workflow/composite/value_budget.rb +90 -0
  76. data/lib/smith/workflow/composite_branch_execution_authorization.rb +52 -0
  77. data/lib/smith/workflow/deadline_enforcement.rb +22 -5
  78. data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
  79. data/lib/smith/workflow/definition_identity.rb +25 -0
  80. data/lib/smith/workflow/deterministic_step.rb +2 -2
  81. data/lib/smith/workflow/dsl.rb +73 -5
  82. data/lib/smith/workflow/durability.rb +1 -1
  83. data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
  84. data/lib/smith/workflow/execution.rb +28 -30
  85. data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
  86. data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
  87. data/lib/smith/workflow/fanout_execution.rb +20 -19
  88. data/lib/smith/workflow/graph/diagnostic.rb +27 -1
  89. data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
  90. data/lib/smith/workflow/graph/execution_successors.rb +65 -0
  91. data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
  92. data/lib/smith/workflow/graph/metrics.rb +1 -3
  93. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
  94. data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
  95. data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
  96. data/lib/smith/workflow/graph/reachability.rb +19 -31
  97. data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
  98. data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
  99. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
  100. data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
  101. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
  102. data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
  103. data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
  104. data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
  105. data/lib/smith/workflow/graph/transition_contract.rb +47 -0
  106. data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
  107. data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
  108. data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
  109. data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
  110. data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
  111. data/lib/smith/workflow/graph/validator.rb +1 -1
  112. data/lib/smith/workflow/graph.rb +62 -4
  113. data/lib/smith/workflow/guardrail_integration.rb +23 -9
  114. data/lib/smith/workflow/identifier.rb +24 -0
  115. data/lib/smith/workflow/message_admission.rb +40 -0
  116. data/lib/smith/workflow/message_admission_boundary.rb +22 -0
  117. data/lib/smith/workflow/message_batch.rb +60 -0
  118. data/lib/smith/workflow/message_value_normalizer.rb +134 -0
  119. data/lib/smith/workflow/nested_execution.rb +10 -0
  120. data/lib/smith/workflow/orchestrator_worker.rb +3 -7
  121. data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
  122. data/lib/smith/workflow/parallel/execution_context.rb +113 -0
  123. data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
  124. data/lib/smith/workflow/parallel/root_execution.rb +91 -0
  125. data/lib/smith/workflow/parallel.rb +19 -22
  126. data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
  127. data/lib/smith/workflow/parallel_execution.rb +46 -14
  128. data/lib/smith/workflow/persistence.rb +37 -8
  129. data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
  130. data/lib/smith/workflow/prepared_step.rb +122 -0
  131. data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
  132. data/lib/smith/workflow/prepared_step_execution_authorization.rb +101 -0
  133. data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
  134. data/lib/smith/workflow/prepared_step_execution_scope.rb +111 -0
  135. data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
  136. data/lib/smith/workflow/process_local.rb +33 -0
  137. data/lib/smith/workflow/retry_execution.rb +20 -11
  138. data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
  139. data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
  140. data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
  141. data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
  142. data/lib/smith/workflow/split_step_persistence/composite_branch_authorization.rb +99 -0
  143. data/lib/smith/workflow/split_step_persistence/composite_branch_effects.rb +41 -0
  144. data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +95 -0
  145. data/lib/smith/workflow/split_step_persistence/composite_branch_outcome.rb +36 -0
  146. data/lib/smith/workflow/split_step_persistence/composite_execution.rb +39 -0
  147. data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +57 -0
  148. data/lib/smith/workflow/split_step_persistence/composite_reduction_execution.rb +122 -0
  149. data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
  150. data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
  151. data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
  152. data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
  153. data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
  154. data/lib/smith/workflow/split_step_persistence/execution.rb +20 -64
  155. data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +109 -0
  156. data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
  157. data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +95 -0
  158. data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +109 -0
  159. data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
  160. data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
  161. data/lib/smith/workflow/split_step_persistence/execution_verification.rb +77 -0
  162. data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
  163. data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
  164. data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
  165. data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
  166. data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
  167. data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
  168. data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
  169. data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
  170. data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
  171. data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
  172. data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
  173. data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
  174. data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +66 -0
  175. data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
  176. data/lib/smith/workflow/split_step_persistence.rb +50 -0
  177. data/lib/smith/workflow/step_completion.rb +31 -0
  178. data/lib/smith/workflow/step_context.rb +46 -0
  179. data/lib/smith/workflow/string_snapshot.rb +25 -0
  180. data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
  181. data/lib/smith/workflow/transition.rb +137 -133
  182. data/lib/smith/workflow/transition_actionability.rb +22 -0
  183. data/lib/smith/workflow.rb +64 -18
  184. data/lib/smith.rb +44 -2
  185. data/sig/smith.rbs +1 -0
  186. metadata +155 -19
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/initializer"
4
+
5
+ require_relative "payload_digest"
6
+
7
+ module Smith
8
+ class Workflow
9
+ module Composite
10
+ class PlanIntegrity
11
+ extend Dry::Initializer
12
+
13
+ param :plan
14
+
15
+ def call
16
+ validate_execution_semantics!
17
+ validate_branches!
18
+ validate_digest!
19
+ plan
20
+ end
21
+
22
+ private
23
+
24
+ def validate_execution_semantics!
25
+ return if plan.execution_semantics_version == Smith::EXECUTION_SEMANTICS_VERSION
26
+
27
+ raise ArgumentError, "composite plan execution semantics do not match"
28
+ end
29
+
30
+ def validate_branches!
31
+ keys = {}
32
+ plan.branches.each_with_index do |branch, ordinal|
33
+ raise ArgumentError, "composite plan ordinals must be contiguous" unless branch.ordinal == ordinal
34
+ raise ArgumentError, "composite plan branch keys must be unique" if keys.key?(branch.key)
35
+
36
+ keys[branch.key] = true
37
+ end
38
+ end
39
+
40
+ def validate_digest!
41
+ attributes = plan.to_h.except(:plan_digest).merge(
42
+ dispatch: plan.dispatch.to_h,
43
+ branches: plan.branches.map(&:to_h)
44
+ )
45
+ return if plan.plan_digest == PayloadDigest.call(attributes)
46
+
47
+ raise ArgumentError, "composite plan digest does not match"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "branch"
6
+ require_relative "plan"
7
+
8
+ module Smith
9
+ class Workflow
10
+ module Composite
11
+ class Planner
12
+ extend Dry::Initializer
13
+
14
+ option :dispatch
15
+ option :kind
16
+ option :transition
17
+ option :from
18
+ option :execution_namespace
19
+ option :branch_specs
20
+ option :input_digest
21
+ option :budget_state_digest
22
+
23
+ def call
24
+ Plan.validate_branch_count!(branch_specs)
25
+ Plan.build(
26
+ dispatch:,
27
+ kind:,
28
+ transition:,
29
+ from:,
30
+ execution_namespace:,
31
+ branches: build_branches,
32
+ input_digest:,
33
+ budget_state_digest:
34
+ )
35
+ end
36
+
37
+ private
38
+
39
+ def build_branches
40
+ branch_specs.each_with_index.map do |spec, ordinal|
41
+ Branch.build(
42
+ ordinal:,
43
+ key: spec.fetch(:key),
44
+ agent: spec.fetch(:agent),
45
+ binding_identity: spec.fetch(:binding_identity),
46
+ budget: spec.fetch(:budget, {})
47
+ )
48
+ end.freeze
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../types"
4
+ require_relative "input"
5
+ require_relative "plan"
6
+
7
+ module Smith
8
+ class Workflow
9
+ module Composite
10
+ class Preparation < Dry::Struct
11
+ attribute :plan, Types.Instance(Plan)
12
+ attribute :input, Types.Instance(Input)
13
+
14
+ def initialize(attributes)
15
+ super
16
+ unless plan.input_digest == input.digest
17
+ raise ArgumentError,
18
+ "composite preparation input does not match plan"
19
+ end
20
+
21
+ self.attributes.freeze
22
+ freeze
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "branch_outcome"
6
+ require_relative "budget_math"
7
+ require_relative "effects"
8
+ require_relative "outcome_accumulator"
9
+ require_relative "plan"
10
+ require_relative "reduction"
11
+
12
+ module Smith
13
+ class Workflow
14
+ module Composite
15
+ class Reducer
16
+ extend Dry::Initializer
17
+
18
+ option :plan
19
+ option :outcomes
20
+ option :primary_failure, optional: true
21
+
22
+ def call
23
+ outcome_set = OutcomeAccumulator.new(plan:, outcomes:).call
24
+ ordered = outcome_set.ordered
25
+ validate_effects!(ordered)
26
+ effects = merged_effects(ordered)
27
+ failure_seen, selected_failure = failure_state(ordered)
28
+ return successful_reduction(outcome_set.output, effects) unless failure_seen
29
+
30
+ failed_reduction(selected_failure, effects)
31
+ end
32
+
33
+ private
34
+
35
+ def validate_effects!(ordered)
36
+ usage_ids = {}
37
+ ordered.each { validate_outcome_effects!(_1, usage_ids) }
38
+ end
39
+
40
+ def validate_outcome_effects!(outcome, usage_ids)
41
+ branch = plan.branches.fetch(outcome.ordinal)
42
+ validate_budget!(outcome.effects, branch.budget)
43
+ validate_usage!(outcome.effects.usage_entries, branch, usage_ids)
44
+ end
45
+
46
+ def validate_usage!(entries, branch, usage_ids)
47
+ entries.each { validate_usage_entry!(_1, branch, usage_ids) }
48
+ end
49
+
50
+ def validate_usage_entry!(entry, branch, usage_ids)
51
+ unless entry.fetch("agent_name") == branch.agent
52
+ raise ArgumentError, "composite usage entry does not match its branch agent"
53
+ end
54
+
55
+ usage_id = entry.fetch("usage_id")
56
+ raise ArgumentError, "composite usage entry is duplicated" if usage_ids.key?(usage_id)
57
+
58
+ usage_ids[usage_id] = true
59
+ end
60
+
61
+ def validate_budget!(effects, envelope)
62
+ consumed = effects.budget_consumed
63
+ validate_budget_dimensions!(consumed, envelope)
64
+ envelope.each { |dimension, limit| validate_budget_dimension!(dimension, limit, consumed, effects) }
65
+ end
66
+
67
+ def validate_budget_dimensions!(consumed, envelope)
68
+ return if (consumed.keys - envelope.keys).empty?
69
+
70
+ raise ArgumentError, "composite branch consumed an undeclared budget dimension"
71
+ end
72
+
73
+ def validate_budget_dimension!(dimension, limit, consumed, effects)
74
+ amount = consumed.fetch(dimension, 0)
75
+ unless amount.is_a?(Numeric) && amount.finite? && amount >= 0 && amount <= limit
76
+ raise ArgumentError, "composite branch budget consumption exceeds its envelope"
77
+ end
78
+
79
+ expected = expected_consumption(dimension, effects)
80
+ return if decimal_equal?(amount, expected)
81
+
82
+ raise ArgumentError, "composite branch budget consumption does not match recorded usage"
83
+ end
84
+
85
+ def expected_consumption(dimension, effects)
86
+ return effects.total_tokens if %w[total_tokens token_limit].include?(dimension)
87
+ return effects.total_cost if dimension == "total_cost"
88
+
89
+ 0
90
+ end
91
+
92
+ def decimal_equal?(left, right)
93
+ Budget::DecimalContext.call { BigDecimal(left.to_s) == BigDecimal(right.to_s) }
94
+ end
95
+
96
+ def merged_effects(ordered)
97
+ Effects.new(
98
+ usage_entries: ordered.flat_map { _1.effects.usage_entries },
99
+ tool_results: ordered.flat_map { _1.effects.tool_results },
100
+ budget_consumed: BudgetMath.sum(ordered.map { _1.effects.budget_consumed })
101
+ )
102
+ end
103
+
104
+ def failure_state(ordered)
105
+ expected_key = primary_failure&.to_s
106
+ failure_seen = false
107
+ selected_failure = nil
108
+ ordered.each do |outcome|
109
+ next unless outcome.failed?
110
+
111
+ failure_seen = true
112
+ selected_failure = outcome if outcome.branch_key == expected_key
113
+ end
114
+ [failure_seen, selected_failure]
115
+ end
116
+
117
+ def successful_reduction(output, effects)
118
+ raise ArgumentError, "primary failure must be absent for a successful composite" if primary_failure
119
+
120
+ Reduction.new(status: :succeeded, output:, error: nil,
121
+ failed_branch_key: nil, effects:)
122
+ end
123
+
124
+ def failed_reduction(failure, effects)
125
+ raise ArgumentError, "primary failure must identify a failed branch" unless failure
126
+
127
+ Reduction.new(status: :failed, output: nil, error: failure.error,
128
+ failed_branch_key: failure.branch_key, effects:)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../types"
4
+ require_relative "effects"
5
+ require_relative "error"
6
+
7
+ module Smith
8
+ class Workflow
9
+ module Composite
10
+ class Reduction < Dry::Struct
11
+ attribute :status, Types::Symbol.enum(:succeeded, :failed)
12
+ attribute? :output, Types::Any.optional
13
+ attribute? :error, Types.Instance(Error).optional
14
+ attribute? :failed_branch_key, Types::String.optional
15
+ attribute :effects, Types.Instance(Effects)
16
+
17
+ def initialize(attributes)
18
+ super
19
+ valid = status == :succeeded ? error.nil? && failed_branch_key.nil? : error && failed_branch_key
20
+ raise ArgumentError, "composite reduction fields do not match status" unless valid
21
+
22
+ self.attributes.freeze
23
+ freeze
24
+ end
25
+
26
+ def succeeded? = status == :succeeded
27
+ def failed? = status == :failed
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "../../errors"
6
+ require_relative "../string_snapshot"
7
+
8
+ module Smith
9
+ class Workflow
10
+ module Composite
11
+ class ValueBudget
12
+ extend Dry::Initializer
13
+
14
+ ARRAY_EACH = Array.instance_method(:each)
15
+ HASH_EACH_PAIR = Hash.instance_method(:each_pair)
16
+ private_constant :ARRAY_EACH, :HASH_EACH_PAIR
17
+
18
+ option :max_bytes
19
+ option :max_nodes
20
+ option :max_depth
21
+ option :label
22
+
23
+ def initialize(...)
24
+ super
25
+ @bytes = 0
26
+ @nodes = 0
27
+ end
28
+
29
+ def add(value, depth: 0)
30
+ pending = [[value, depth]]
31
+ until pending.empty?
32
+ item, item_depth = pending.pop
33
+ visit!(item, item_depth, pending)
34
+ end
35
+ self
36
+ end
37
+
38
+ private
39
+
40
+ def visit!(item, depth, pending)
41
+ validate_visit!(depth)
42
+ dispatch_value(item, depth, pending)
43
+ end
44
+
45
+ def dispatch_value(item, depth, pending)
46
+ case item
47
+ when Hash then enqueue_hash(item, depth, pending)
48
+ when Array then enqueue_array(item, depth, pending)
49
+ when String, Symbol then add_bytes!(item.to_s)
50
+ when Float then validate_float!(item)
51
+ when Integer, true, false, nil then nil
52
+ else raise WorkflowError, "#{label} contains unsupported value #{item.class}"
53
+ end
54
+ end
55
+
56
+ def validate_visit!(depth)
57
+ raise WorkflowError, "#{label} exceeds maximum depth #{max_depth}" if depth > max_depth
58
+
59
+ @nodes += 1
60
+ raise WorkflowError, "#{label} exceeds maximum size #{max_nodes}" if @nodes > max_nodes
61
+ end
62
+
63
+ def enqueue_hash(hash, depth, pending)
64
+ HASH_EACH_PAIR.bind_call(hash) do |key, value|
65
+ unless key.is_a?(String) || key.is_a?(Symbol)
66
+ raise WorkflowError, "#{label} contains unsupported Hash key #{key.class}"
67
+ end
68
+
69
+ pending << [value, depth + 1] << [key, depth + 1]
70
+ end
71
+ end
72
+
73
+ def enqueue_array(array, depth, pending)
74
+ ARRAY_EACH.bind_call(array) { |value| pending << [value, depth + 1] }
75
+ end
76
+
77
+ def add_bytes!(string)
78
+ @bytes += StringSnapshot.bytesize(string)
79
+ raise WorkflowError, "#{label} exceeds maximum bytes #{max_bytes}" if @bytes > max_bytes
80
+ end
81
+
82
+ def validate_float!(float)
83
+ return if float.finite?
84
+
85
+ raise WorkflowError, "#{label} contains a non-finite Float"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module DefinitionIdentity
6
+ module ClassMethods
7
+ def inherited(subclass)
8
+ super
9
+ digest = definition_identity_mutex.synchronize { @definition_digest }
10
+ subclass.instance_variable_set(:@definition_digest, digest)
11
+ subclass.instance_variable_set(:@definition_identity_mutex, Mutex.new)
12
+ subclass.instance_variable_set(:@definition_identity_sealed, false)
13
+ end
14
+
15
+ def definition_digest(value = nil)
16
+ return definition_identity_mutex.synchronize { @definition_digest } if value.nil?
17
+
18
+ unless Smith::Types::Sha256Hex.valid?(value)
19
+ raise ArgumentError, "definition_digest must be a lowercase SHA-256 hex digest"
20
+ end
21
+
22
+ digest = value.dup.freeze
23
+ definition_identity_mutex.synchronize do
24
+ return @definition_digest if @definition_digest == digest
25
+ if @definition_identity_sealed || frozen?
26
+ raise ArgumentError, "definition_digest is sealed for this workflow class"
27
+ end
28
+
29
+ @definition_digest = digest
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def seal_definition_identity!
36
+ definition_identity_mutex.synchronize do
37
+ @definition_identity_sealed = true unless frozen?
38
+ @definition_digest
39
+ end
40
+ end
41
+
42
+ def definition_identity_mutex
43
+ @definition_identity_mutex ||= Mutex.new
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "definition_identity/class_methods"
4
+
5
+ module Smith
6
+ class Workflow
7
+ module DefinitionIdentity
8
+ def self.included(base)
9
+ base.instance_variable_set(:@definition_identity_mutex, Mutex.new)
10
+ base.instance_variable_set(:@definition_identity_sealed, false)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ private
15
+
16
+ def effective_definition_digest
17
+ if instance_variable_defined?(:@split_step_definition_digest)
18
+ @split_step_definition_digest
19
+ else
20
+ self.class.definition_digest
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -23,8 +23,8 @@ module Smith
23
23
  def last_output
24
24
  return @last_output if defined?(@last_output)
25
25
 
26
- msg = session_messages.reverse.find { |m| m[:role] == :assistant || m[:role] == "assistant" }
27
- @last_output = msg&.dig(:content)
26
+ msg = session_messages.reverse.find { |m| m[:role] == :assistant || m["role"] == "assistant" }
27
+ @last_output = msg && (msg[:content] || msg["content"])
28
28
  end
29
29
 
30
30
  alias_method :output, :last_output