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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "dry-initializer"
5
+
6
+ require_relative "../../budget/decimal_context"
7
+ require_relative "../../budget/ledger"
8
+ require_relative "plan"
9
+
10
+ module Smith
11
+ class Workflow
12
+ module Composite
13
+ class BudgetAllocator
14
+ extend Dry::Initializer
15
+
16
+ TOKEN_DIMENSIONS = %i[total_tokens token_limit].freeze
17
+ SUPPORTED_DIMENSIONS = (TOKEN_DIMENSIONS + %i[total_cost]).freeze
18
+
19
+ option :ledger
20
+ option :branch_count
21
+
22
+ def initialize(...)
23
+ super
24
+ unless ledger.is_a?(Budget::Ledger)
25
+ raise ArgumentError, "composite budget allocation requires a budget ledger"
26
+ end
27
+
28
+ Plan.validate_branch_count!(branch_count)
29
+ @remaining = ledger.limits.to_h { |dimension, _| [dimension, ledger.remaining(dimension)] }.freeze
30
+ end
31
+
32
+ def call(ordinal:, caps: {})
33
+ validate_ordinal!(ordinal)
34
+
35
+ @remaining.to_h do |dimension, amount|
36
+ [dimension, capped_share(dimension, amount, ordinal, caps[dimension])]
37
+ end.freeze
38
+ end
39
+
40
+ private
41
+
42
+ def validate_ordinal!(ordinal)
43
+ return if ordinal.is_a?(Integer) && ordinal.between?(0, branch_count - 1)
44
+
45
+ raise ArgumentError, "composite branch ordinal is outside the allocation"
46
+ end
47
+
48
+ def capped_share(dimension, amount, ordinal, cap)
49
+ unless cap.nil? || (cap.is_a?(Numeric) && cap.finite? && cap >= 0)
50
+ raise ArgumentError, "composite branch budget cap must be a finite non-negative number"
51
+ end
52
+
53
+ allocation = share(dimension, amount, ordinal)
54
+ cap.nil? ? allocation : [allocation, cap].min
55
+ end
56
+
57
+ def share(dimension, amount, ordinal)
58
+ return 0 unless SUPPORTED_DIMENSIONS.include?(dimension)
59
+ return integer_share(amount, ordinal) if TOKEN_DIMENSIONS.include?(dimension) && amount.is_a?(Integer)
60
+
61
+ decimal_share(amount)
62
+ end
63
+
64
+ def integer_share(amount, ordinal)
65
+ quotient, remainder = amount.divmod(branch_count)
66
+ quotient + (ordinal < remainder ? 1 : 0)
67
+ end
68
+
69
+ def decimal_share(amount)
70
+ exact = Budget::DecimalContext.call { BigDecimal(amount.to_s) / branch_count }
71
+ candidate = exact.to_f
72
+ candidate = candidate.prev_float if BigDecimal(candidate.to_s) > exact
73
+ candidate
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "dry-initializer"
5
+
6
+ require_relative "../../budget/decimal_context"
7
+
8
+ module Smith
9
+ class Workflow
10
+ module Composite
11
+ class BudgetMath
12
+ extend Dry::Initializer
13
+
14
+ HASH_EACH_PAIR = Hash.instance_method(:each_pair)
15
+ private_constant :HASH_EACH_PAIR
16
+
17
+ param :consumptions
18
+
19
+ def self.sum(consumptions) = new(consumptions).sum
20
+
21
+ def sum
22
+ Budget::DecimalContext.call do
23
+ totals = {}
24
+ integer_dimensions = {}
25
+ consumptions.each { accumulate!(_1, totals, integer_dimensions) }
26
+ externalize(totals, integer_dimensions)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def accumulate!(value, totals, integer_dimensions)
33
+ raise ArgumentError, "composite budget consumption must be a Hash" unless value.is_a?(Hash)
34
+
35
+ HASH_EACH_PAIR.bind_call(value) do |dimension, amount|
36
+ key = dimension.to_s
37
+ totals[key] = totals.fetch(key, BigDecimal("0")) + BigDecimal(amount.to_s)
38
+ integer_dimensions[key] = integer_dimensions.fetch(key, true) && amount.is_a?(Integer)
39
+ end
40
+ end
41
+
42
+ def externalize(totals, integer_dimensions)
43
+ totals.to_h do |dimension, total|
44
+ value = integer_dimensions.fetch(dimension) ? total.to_i : total.to_f
45
+ [dimension, value]
46
+ end.freeze
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "branch_contract"
4
+ require_relative "branch_execution"
5
+ require_relative "execution_contract"
6
+ require_relative "input"
7
+ require_relative "plan"
8
+ require_relative "../composite_branch_execution_authorization"
9
+
10
+ module Smith
11
+ class Workflow
12
+ module Composite
13
+ module Contract
14
+ include BranchContract
15
+ include ExecutionContract
16
+
17
+ private
18
+
19
+ def validate_composite_authorization!(authorization)
20
+ authorization = validate_split_step_execution_authorization!(authorization)
21
+ @split_step_mutex.synchronize do
22
+ unless active_split_step_execution_authorization?(authorization)
23
+ raise WorkflowError, "the prepared-step execution authorization is no longer active"
24
+ end
25
+
26
+ ensure_split_step_definition_current!
27
+ ensure_prepared_split_step_transition_matches!
28
+ end
29
+ validate_composite_transition!(@split_step_transition)
30
+ unless authorization.dispatch_claim
31
+ raise WorkflowError, "durable composite execution requires a prepared-step dispatch"
32
+ end
33
+
34
+ authorization
35
+ end
36
+
37
+ def validate_composite_transition!(transition)
38
+ unless transition&.fanout? || transition&.parallel?
39
+ raise WorkflowError, "prepared transition is not a supported composite"
40
+ end
41
+ raise WorkflowError, "durable composite retries are not supported" if transition.retry_config
42
+
43
+ transition
44
+ end
45
+
46
+ def validate_composite_plan!(authorization, plan, input)
47
+ validate_composite_payload_types!(plan, input)
48
+ validate_composite_dispatch!(authorization, plan)
49
+ validate_composite_input!(plan, input)
50
+ validate_composite_budget_state!(plan)
51
+ validate_composite_execution_namespace!(plan)
52
+ transition = @split_step_transition
53
+ validate_composite_transition_identity!(plan, transition)
54
+ plan
55
+ end
56
+
57
+ def validate_composite_branch_execution!(authorization, execution, input)
58
+ validate_composite_branch_payload_types!(execution, input)
59
+ validate_composite_dispatch_value!(authorization, execution.dispatch)
60
+ validate_composite_input_digest!(execution.input_digest, input)
61
+ validate_composite_budget_state_digest!(execution.budget_state_digest)
62
+ validate_composite_execution_namespace_value!(execution.execution_namespace)
63
+ validate_composite_transition_values!(execution, @split_step_transition)
64
+ validate_composite_branch_count!(execution.branch_count)
65
+ validate_composite_branch_authorization!(authorization, execution, input)
66
+ validate_composite_selected_branch!(authorization, execution, @split_step_transition)
67
+ execution
68
+ end
69
+
70
+ def validate_composite_reduction_plan!(authorization, plan, input)
71
+ validate_composite_plan!(authorization, plan, input)
72
+ validate_composite_branches!(authorization, plan, @split_step_transition)
73
+ plan
74
+ end
75
+
76
+ def validate_composite_payload_types!(plan, input)
77
+ raise ArgumentError, "plan must be a Smith composite plan" unless plan.is_a?(Plan)
78
+ raise ArgumentError, "input must be a Smith composite input" unless input.is_a?(Input)
79
+ end
80
+
81
+ def validate_composite_branch_payload_types!(execution, input)
82
+ unless execution.is_a?(BranchExecution)
83
+ raise ArgumentError, "execution must be a Smith composite branch execution"
84
+ end
85
+ raise ArgumentError, "input must be a Smith composite input" unless input.is_a?(Input)
86
+ end
87
+
88
+ def validate_composite_branch_authorization!(authorization, execution, input)
89
+ unless authorization.instance_of?(CompositeBranchExecutionAuthorization)
90
+ raise ArgumentError, "authorization must be a Smith composite branch authorization"
91
+ end
92
+
93
+ CompositeBranchExecutionAuthorization
94
+ .instance_method(:verify_composite_branch!)
95
+ .bind_call(authorization, execution, input)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../types"
4
+ require_relative "../../budget/decimal_context"
5
+ require_relative "../message_value_normalizer"
6
+ require_relative "../prepared_step"
7
+ require_relative "payload"
8
+
9
+ module Smith
10
+ class Workflow
11
+ module Composite
12
+ class Effects < Payload
13
+ attr_reader :total_tokens, :total_cost
14
+
15
+ USAGE_ATTRIBUTES = %w[
16
+ usage_id agent_name model input_tokens output_tokens cost attempt_kind recorded_at
17
+ ].freeze
18
+ TOOL_ATTRIBUTES = %w[tool captured].freeze
19
+ private_constant :USAGE_ATTRIBUTES, :TOOL_ATTRIBUTES
20
+
21
+ attribute :usage_entries, Types::Array
22
+ attribute :tool_results, Types::Array
23
+ attribute :budget_consumed, Types::Hash
24
+
25
+ def initialize(attributes)
26
+ owned = self.class.normalize_attributes(attributes)
27
+ normalized = MessageValueNormalizer.new(owned, label: "composite effects").call
28
+ usage_entries = normalized.fetch("usage_entries")
29
+ tool_results = normalized.fetch("tool_results")
30
+ budget_consumed = normalized.fetch("budget_consumed")
31
+ @total_tokens, @total_cost = validate_usage_entries!(usage_entries)
32
+ validate_tool_results!(tool_results)
33
+ validate_budget!(budget_consumed)
34
+ super(
35
+ usage_entries:,
36
+ tool_results:,
37
+ budget_consumed:
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ def validate_usage_entries!(entries)
44
+ raise ArgumentError, "composite usage entries must be an Array" unless entries.is_a?(Array)
45
+
46
+ entries.each do |entry|
47
+ validate_exact_keys!(entry, USAGE_ATTRIBUTES, "composite usage entry")
48
+ validate_usage_identity!(entry)
49
+ validate_usage_amount!(entry.fetch("input_tokens"), "input_tokens")
50
+ validate_usage_amount!(entry.fetch("output_tokens"), "output_tokens")
51
+ validate_cost!(entry.fetch("cost"))
52
+ end
53
+ usage_totals(entries)
54
+ end
55
+
56
+ def usage_totals(entries)
57
+ tokens = entries.sum { _1.fetch("input_tokens") + _1.fetch("output_tokens") }
58
+ if tokens > PreparedStep::MAX_COUNTER_VALUE
59
+ raise ArgumentError, "composite usage token total exceeds the signed 64-bit limit"
60
+ end
61
+
62
+ cost = Budget::DecimalContext.call do
63
+ entries.sum(BigDecimal("0")) { BigDecimal((_1.fetch("cost") || 0).to_s) }
64
+ end.to_f
65
+ raise ArgumentError, "composite usage cost total must be finite" unless cost.finite?
66
+
67
+ [tokens, cost]
68
+ end
69
+
70
+ def validate_usage_identity!(entry)
71
+ validate_usage_id!(entry.fetch("usage_id"))
72
+ validate_agent_name!(entry.fetch("agent_name"))
73
+ %w[model attempt_kind recorded_at].each do |key|
74
+ validate_nonempty_string!(entry.fetch(key), "composite usage entry #{key}")
75
+ end
76
+ end
77
+
78
+ def validate_usage_id!(usage_id)
79
+ return if usage_id.is_a?(String) && PreparedStep::UUID_PATTERN.match?(usage_id)
80
+
81
+ raise ArgumentError, "composite usage entry usage_id must be a UUID"
82
+ end
83
+
84
+ def validate_agent_name!(agent_name)
85
+ return if agent_name.nil?
86
+
87
+ validate_nonempty_string!(agent_name, "composite usage entry agent_name")
88
+ end
89
+
90
+ def validate_nonempty_string!(value, label)
91
+ return if value.is_a?(String) && !value.empty?
92
+
93
+ raise ArgumentError, "#{label} must be a non-empty String"
94
+ end
95
+
96
+ def validate_usage_amount!(amount, name)
97
+ return if amount.is_a?(Integer) && amount >= 0
98
+
99
+ raise ArgumentError, "composite usage entry #{name} must be a non-negative Integer"
100
+ end
101
+
102
+ def validate_cost!(cost)
103
+ return if cost.nil?
104
+ return if cost.is_a?(Numeric) && cost.finite? && cost >= 0
105
+
106
+ raise ArgumentError, "composite usage entry cost must be a finite non-negative number or nil"
107
+ end
108
+
109
+ def validate_tool_results!(entries)
110
+ raise ArgumentError, "composite tool results must be an Array" unless entries.is_a?(Array)
111
+
112
+ entries.each do |entry|
113
+ validate_exact_keys!(entry, TOOL_ATTRIBUTES, "composite tool result")
114
+ tool = entry.fetch("tool")
115
+ unless tool.is_a?(String) && tool.length.between?(1, 256)
116
+ raise ArgumentError, "composite tool result tool must be a bounded non-empty String"
117
+ end
118
+ end
119
+ end
120
+
121
+ def validate_budget!(budget)
122
+ raise ArgumentError, "composite budget consumption must be a Hash" unless budget.is_a?(Hash)
123
+
124
+ budget.each do |dimension, amount|
125
+ unless !dimension.empty? && amount.is_a?(Numeric) && amount.finite? && amount >= 0
126
+ raise ArgumentError, "composite budget consumption is invalid"
127
+ end
128
+ end
129
+ end
130
+
131
+ def validate_exact_keys!(value, expected, label)
132
+ raise ArgumentError, "#{label} must be a Hash" unless value.is_a?(Hash)
133
+ return if value.keys.sort == expected.sort
134
+
135
+ raise ArgumentError, "#{label} attributes are invalid"
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../types"
4
+ require_relative "../../budget/ledger"
5
+ require_relative "../usage_entry"
6
+ require_relative "effects_baseline"
7
+
8
+ module Smith
9
+ class Workflow
10
+ module Composite
11
+ class EffectsApplication < Dry::Struct
12
+ NumericType = Types::Integer | Types::Float
13
+ private_constant :NumericType
14
+
15
+ attribute :usage_entries, Types::Array.of(Types.Instance(UsageEntry))
16
+ attribute :tool_results, Types::Array.of(Types::Hash)
17
+ attribute :total_tokens, Types::Integer
18
+ attribute :total_cost, NumericType
19
+ attribute :ledger, Types.Instance(Budget::Ledger).optional
20
+ attribute :baseline, Types.Instance(EffectsBaseline)
21
+
22
+ def initialize(attributes)
23
+ super
24
+ unless total_tokens >= 0 && total_cost.finite? && total_cost >= 0
25
+ raise ArgumentError, "composite effects application totals are invalid"
26
+ end
27
+
28
+ usage_entries.freeze
29
+ tool_results.freeze
30
+ self.attributes.freeze
31
+ freeze
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../types"
4
+ require_relative "../../budget/ledger"
5
+ require_relative "../usage_entry"
6
+
7
+ module Smith
8
+ class Workflow
9
+ module Composite
10
+ class EffectsBaseline < Dry::Struct
11
+ NumericType = Types::Integer | Types::Float
12
+ private_constant :NumericType
13
+
14
+ attribute :usage_entries, Types::Array.of(Types.Instance(UsageEntry))
15
+ attribute :tool_results, Types::Array.of(Types::Hash)
16
+ attribute :total_tokens, Types::Integer
17
+ attribute :total_cost, NumericType
18
+ attribute :ledger, Types.Instance(Budget::Ledger).optional
19
+ attribute :budget_consumed, Types::Hash
20
+
21
+ def initialize(attributes)
22
+ super
23
+ unless total_tokens >= 0 && total_cost.finite? && total_cost >= 0
24
+ raise ArgumentError, "composite effects baseline totals are invalid"
25
+ end
26
+
27
+ usage_entries.freeze
28
+ tool_results.freeze
29
+ budget_consumed.freeze
30
+ self.attributes.freeze
31
+ freeze
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "dry-initializer"
5
+
6
+ require_relative "../../budget/decimal_context"
7
+ require_relative "../../budget/ledger"
8
+ require_relative "../prepared_step"
9
+ require_relative "../usage_entry"
10
+ require_relative "budget_math"
11
+ require_relative "effects_application"
12
+
13
+ module Smith
14
+ class Workflow
15
+ module Composite
16
+ class EffectsPreflight
17
+ extend Dry::Initializer
18
+
19
+ option :effects
20
+ option :baseline
21
+ option :snapshotter
22
+
23
+ def call
24
+ next_total_tokens = total_tokens
25
+ next_total_cost = total_cost
26
+ next_usage = incoming_usage
27
+ next_ledger = ledger
28
+ next_tools = incoming_tools
29
+ tool_results = baseline.tool_results + next_tools
30
+ EffectsApplication.new(
31
+ usage_entries: baseline.usage_entries + next_usage,
32
+ tool_results:,
33
+ total_tokens: next_total_tokens,
34
+ total_cost: next_total_cost,
35
+ ledger: next_ledger,
36
+ baseline:
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def incoming_usage
43
+ @incoming_usage ||= effects.usage_entries.map { UsageEntry.from_h(_1) }.tap do |entries|
44
+ known = baseline.usage_entries.to_h { [_1.usage_id, true] }
45
+ raise WorkflowError, "composite usage entry was already applied" if
46
+ entries.any? { known.key?(_1.usage_id) }
47
+ end
48
+ end
49
+
50
+ def incoming_tools
51
+ @incoming_tools ||= effects.tool_results.map do |entry|
52
+ snapshotter.call(entry).transform_keys { _1.respond_to?(:to_sym) ? _1.to_sym : _1 }
53
+ end
54
+ end
55
+
56
+ def total_tokens
57
+ current = baseline.total_tokens
58
+ raise WorkflowError, "workflow token total is invalid" unless current.is_a?(Integer) && current >= 0
59
+
60
+ total = current + effects.total_tokens
61
+ return total if total <= PreparedStep::MAX_COUNTER_VALUE
62
+
63
+ raise WorkflowError, "workflow token total exceeds the signed 64-bit limit"
64
+ end
65
+
66
+ def total_cost
67
+ current = baseline.total_cost
68
+ unless current.is_a?(Numeric) && current.finite? && current >= 0
69
+ raise WorkflowError, "workflow cost total is invalid"
70
+ end
71
+
72
+ total = Budget::DecimalContext.call do
73
+ BigDecimal(current.to_s) + BigDecimal(effects.total_cost.to_s)
74
+ end.to_f
75
+ return total if total.finite?
76
+
77
+ raise WorkflowError, "workflow cost total must be finite"
78
+ end
79
+
80
+ def ledger
81
+ return baseline.ledger if effects.budget_consumed.empty?
82
+ raise WorkflowError, "composite budget effects require a workflow ledger" unless baseline.ledger
83
+
84
+ Budget::Ledger.new(
85
+ limits: baseline.ledger.limits,
86
+ consumed: combined_budget_consumed
87
+ )
88
+ end
89
+
90
+ def combined_budget_consumed
91
+ combined = BudgetMath
92
+ .sum([baseline.budget_consumed, effects.budget_consumed])
93
+ .transform_keys(&:to_sym)
94
+ combined.each do |dimension, amount|
95
+ raise BudgetExceeded if amount > baseline.ledger.limits.fetch(dimension)
96
+ end
97
+ combined
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+ require "json"
5
+
6
+ require_relative "../../errors"
7
+
8
+ module Smith
9
+ class Workflow
10
+ module Composite
11
+ class EncodedValueBudget
12
+ extend Dry::Initializer
13
+
14
+ option :max_bytes
15
+ option :label
16
+
17
+ def initialize(...)
18
+ super
19
+ @bytes = 2
20
+ @entries = 0
21
+ end
22
+
23
+ def add(value)
24
+ @bytes += JSON.generate(value, max_nesting: false).bytesize
25
+ @bytes += 1 if @entries.positive?
26
+ @entries += 1
27
+ raise WorkflowError, "#{label} exceeds maximum bytes #{max_bytes}" if @bytes > max_bytes
28
+
29
+ self
30
+ rescue JSON::GeneratorError => e
31
+ raise WorkflowError, "#{label} cannot be encoded: #{e.message}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module Composite
6
+ module Enums
7
+ VALUES = {
8
+ kind: { "parallel" => :parallel, "fanout" => :fanout }.freeze,
9
+ status: { "succeeded" => :succeeded, "failed" => :failed }.freeze,
10
+ resume_policy: { "incomplete_only" => :incomplete_only }.freeze,
11
+ failure_policy: { "host_committed_primary" => :host_committed_primary }.freeze,
12
+ reduction_policy: { "ordered_all_success" => :ordered_all_success }.freeze,
13
+ retry_policy: { "none" => :none }.freeze
14
+ }.freeze
15
+ private_constant :VALUES
16
+
17
+ def self.normalize(name, value)
18
+ return value unless value.is_a?(String)
19
+
20
+ VALUES.fetch(name).fetch(value, value)
21
+ end
22
+ end
23
+
24
+ private_constant :Enums
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../types"
4
+ require_relative "payload"
5
+
6
+ module Smith
7
+ class Workflow
8
+ module Composite
9
+ class Error < Payload
10
+ FAMILIES = %w[
11
+ tool_guardrail_failed deterministic_step_failure deadline_exceeded
12
+ agent_error workflow_error other
13
+ ].freeze
14
+ ALWAYS_RETRYABLE_FAMILIES = %w[deadline_exceeded agent_error].freeze
15
+ CONDITIONALLY_RETRYABLE_FAMILIES = %w[tool_guardrail_failed deterministic_step_failure].freeze
16
+ private_constant :ALWAYS_RETRYABLE_FAMILIES, :CONDITIONALLY_RETRYABLE_FAMILIES
17
+ OwnedString = Types::String.constructor { |value| value.is_a?(String) ? value.dup.freeze : value }
18
+ private_constant :OwnedString
19
+
20
+ attribute :class_name, OwnedString.constrained(min_size: 1, max_size: 256)
21
+ attribute :family, OwnedString.enum(*FAMILIES)
22
+ attribute :retryable, Types::Bool
23
+ attribute? :kind, OwnedString.constrained(min_size: 1, max_size: 128).optional
24
+
25
+ def initialize(attributes)
26
+ owned = self.class.normalize_attributes(attributes)
27
+ owned[:kind] = owned[:kind].to_s if owned[:kind]
28
+ super(owned)
29
+ validate_retryability!
30
+ validate_kind!
31
+ end
32
+
33
+ private
34
+
35
+ def validate_retryability!
36
+ valid = if ALWAYS_RETRYABLE_FAMILIES.include?(family)
37
+ retryable
38
+ elsif CONDITIONALLY_RETRYABLE_FAMILIES.include?(family)
39
+ [true, false].include?(retryable)
40
+ else
41
+ !retryable
42
+ end
43
+ raise ArgumentError, "composite error retryability does not match its family" unless valid
44
+ end
45
+
46
+ def validate_kind!
47
+ return if kind.nil? || family == "deterministic_step_failure"
48
+
49
+ raise ArgumentError, "composite error kind is not valid for its family"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end