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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +83 -0
- data/lib/smith/agent.rb +13 -0
- data/lib/smith/errors.rb +4 -0
- data/lib/smith/version.rb +2 -2
- data/lib/smith/workflow/composite/branch.rb +93 -0
- data/lib/smith/workflow/composite/branch_budget_contract.rb +41 -0
- data/lib/smith/workflow/composite/branch_contract.rb +102 -0
- data/lib/smith/workflow/composite/branch_execution.rb +124 -0
- data/lib/smith/workflow/composite/branch_failure.rb +91 -0
- data/lib/smith/workflow/composite/branch_outcome.rb +108 -0
- data/lib/smith/workflow/composite/budget_allocator.rb +78 -0
- data/lib/smith/workflow/composite/budget_math.rb +51 -0
- data/lib/smith/workflow/composite/contract.rb +100 -0
- data/lib/smith/workflow/composite/effects.rb +140 -0
- data/lib/smith/workflow/composite/effects_application.rb +36 -0
- data/lib/smith/workflow/composite/effects_baseline.rb +36 -0
- data/lib/smith/workflow/composite/effects_preflight.rb +102 -0
- data/lib/smith/workflow/composite/encoded_value_budget.rb +36 -0
- data/lib/smith/workflow/composite/enums.rb +27 -0
- data/lib/smith/workflow/composite/error.rb +54 -0
- data/lib/smith/workflow/composite/error_evidence.rb +61 -0
- data/lib/smith/workflow/composite/execution_contract.rb +73 -0
- data/lib/smith/workflow/composite/fanout_branch_contract.rb +61 -0
- data/lib/smith/workflow/composite/input.rb +53 -0
- data/lib/smith/workflow/composite/outcome_accumulator.rb +131 -0
- data/lib/smith/workflow/composite/outcome_set.rb +23 -0
- data/lib/smith/workflow/composite/payload.rb +128 -0
- data/lib/smith/workflow/composite/payload_digest.rb +28 -0
- data/lib/smith/workflow/composite/plan.rb +130 -0
- data/lib/smith/workflow/composite/plan_integrity.rb +52 -0
- data/lib/smith/workflow/composite/planner.rb +53 -0
- data/lib/smith/workflow/composite/preparation.rb +27 -0
- data/lib/smith/workflow/composite/reducer.rb +133 -0
- data/lib/smith/workflow/composite/reduction.rb +31 -0
- data/lib/smith/workflow/composite/value_budget.rb +90 -0
- data/lib/smith/workflow/composite_branch_execution_authorization.rb +52 -0
- data/lib/smith/workflow/deadline_enforcement.rb +22 -5
- data/lib/smith/workflow/execution.rb +16 -16
- data/lib/smith/workflow/fanout_execution.rb +19 -18
- data/lib/smith/workflow/message_value_normalizer.rb +11 -10
- data/lib/smith/workflow/parallel_execution.rb +41 -13
- data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
- data/lib/smith/workflow/prepared_step_execution_authorization.rb +14 -29
- data/lib/smith/workflow/prepared_step_execution_scope.rb +81 -15
- data/lib/smith/workflow/process_local.rb +33 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_authorization.rb +99 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_effects.rb +41 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_outcome.rb +36 -0
- data/lib/smith/workflow/split_step_persistence/composite_execution.rb +39 -0
- data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/composite_reduction_execution.rb +122 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +7 -53
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +41 -21
- data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +5 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +17 -3
- data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +17 -5
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +8 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +21 -5
- data/lib/smith/workflow/split_step_persistence.rb +14 -0
- data/lib/smith/workflow/step_context.rb +46 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
- data/lib/smith/workflow/transition.rb +16 -1
- data/lib/smith/workflow.rb +21 -1
- data/lib/smith.rb +3 -0
- metadata +46 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../errors"
|
|
4
|
+
require_relative "error"
|
|
5
|
+
|
|
6
|
+
module Smith
|
|
7
|
+
class Workflow
|
|
8
|
+
module Composite
|
|
9
|
+
class BranchFailure < WorkflowError
|
|
10
|
+
DETAIL_KEYS = {
|
|
11
|
+
"branch_key" => :branch_key,
|
|
12
|
+
"error_class" => :error_class,
|
|
13
|
+
"error_family" => :error_family,
|
|
14
|
+
"retryable" => :retryable,
|
|
15
|
+
"kind" => :kind
|
|
16
|
+
}.freeze
|
|
17
|
+
DETAIL_NAMES = DETAIL_KEYS.values.freeze
|
|
18
|
+
private_constant :DETAIL_KEYS, :DETAIL_NAMES
|
|
19
|
+
|
|
20
|
+
attr_reader :branch_key, :error_class, :error_family, :retryable, :kind, :details
|
|
21
|
+
|
|
22
|
+
def self.from_details(details)
|
|
23
|
+
values = normalize_details(details)
|
|
24
|
+
error = Error.new(
|
|
25
|
+
class_name: values.fetch(:error_class),
|
|
26
|
+
family: values.fetch(:error_family),
|
|
27
|
+
retryable: values.fetch(:retryable),
|
|
28
|
+
kind: values.fetch(:kind)
|
|
29
|
+
)
|
|
30
|
+
new(branch_key: values.fetch(:branch_key), error:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.normalize_details(details)
|
|
34
|
+
raise ArgumentError, "composite branch failure details must be a Hash" unless details.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
normalized = {}
|
|
37
|
+
Hash.instance_method(:each_pair).bind_call(details) do |key, value|
|
|
38
|
+
name = normalize_detail_key(key)
|
|
39
|
+
if normalized.key?(name)
|
|
40
|
+
raise ArgumentError, "composite branch failure details contain a duplicate attribute"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
normalized[name] = value
|
|
44
|
+
end
|
|
45
|
+
missing = DETAIL_NAMES - normalized.keys
|
|
46
|
+
raise ArgumentError, "composite branch failure details are missing required attributes" if missing.any?
|
|
47
|
+
|
|
48
|
+
normalized
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.normalize_detail_key(key)
|
|
52
|
+
name = key.is_a?(Symbol) ? key : DETAIL_KEYS.fetch(key, key)
|
|
53
|
+
return name if DETAIL_NAMES.include?(name)
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, "composite branch failure details contain an unknown attribute"
|
|
56
|
+
end
|
|
57
|
+
private_class_method :normalize_details
|
|
58
|
+
private_class_method :normalize_detail_key
|
|
59
|
+
|
|
60
|
+
def initialize(branch_key:, error:)
|
|
61
|
+
validate_arguments!(branch_key, error)
|
|
62
|
+
@branch_key = branch_key.dup.freeze
|
|
63
|
+
@error_class = error.class_name.dup.freeze
|
|
64
|
+
@error_family = error.family.dup.freeze
|
|
65
|
+
@retryable = error.retryable
|
|
66
|
+
@kind = error.kind&.dup&.freeze
|
|
67
|
+
@details = failure_details
|
|
68
|
+
super("composite branch #{branch_key.inspect} failed")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def validate_arguments!(branch_key, error)
|
|
74
|
+
valid_key = branch_key.is_a?(String) && branch_key.length.between?(1, 256)
|
|
75
|
+
raise ArgumentError, "composite branch failure key must be a bounded non-empty String" unless valid_key
|
|
76
|
+
raise ArgumentError, "composite branch failure requires typed error evidence" unless error.is_a?(Error)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def failure_details
|
|
80
|
+
{
|
|
81
|
+
branch_key: @branch_key,
|
|
82
|
+
error_class: @error_class,
|
|
83
|
+
error_family: @error_family,
|
|
84
|
+
retryable: @retryable,
|
|
85
|
+
kind: @kind
|
|
86
|
+
}.freeze
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../types"
|
|
4
|
+
require_relative "../message_value_normalizer"
|
|
5
|
+
require_relative "../prepared_step"
|
|
6
|
+
require_relative "effects"
|
|
7
|
+
require_relative "enums"
|
|
8
|
+
require_relative "error"
|
|
9
|
+
require_relative "payload"
|
|
10
|
+
require_relative "payload_digest"
|
|
11
|
+
|
|
12
|
+
module Smith
|
|
13
|
+
class Workflow
|
|
14
|
+
module Composite
|
|
15
|
+
class BranchOutcome < Payload
|
|
16
|
+
OwnedString = Types::String.constructor { |value| value.is_a?(String) ? value.dup.freeze : value }
|
|
17
|
+
private_constant :OwnedString
|
|
18
|
+
|
|
19
|
+
attribute :plan_digest, OwnedString.constrained(format: PreparedStep::DIGEST_PATTERN)
|
|
20
|
+
attribute :branch_digest, OwnedString.constrained(format: PreparedStep::DIGEST_PATTERN)
|
|
21
|
+
attribute :ordinal, Types::Integer.constrained(gteq: 0, lteq: PreparedStep::MAX_COUNTER_VALUE)
|
|
22
|
+
attribute :branch_key, OwnedString.constrained(min_size: 1, max_size: 256)
|
|
23
|
+
attribute :agent, OwnedString.constrained(min_size: 1, max_size: 256)
|
|
24
|
+
attribute :status, Types::Symbol.enum(:succeeded, :failed)
|
|
25
|
+
attribute? :output, Types::Any.optional
|
|
26
|
+
attribute? :error, Types.Instance(Error).optional
|
|
27
|
+
attribute :effects, Types.Instance(Effects)
|
|
28
|
+
attribute :digest, OwnedString.constrained(format: PreparedStep::DIGEST_PATTERN)
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def succeeded(plan_digest:, branch:, output:, effects:)
|
|
32
|
+
build(plan_digest:, branch:, effects:, status: :succeeded, output:, error: nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def failed(plan_digest:, branch:, error:, effects:)
|
|
36
|
+
build(plan_digest:, branch:, effects:, status: :failed, output: nil, error:)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalize_attributes(attributes)
|
|
40
|
+
normalized = super
|
|
41
|
+
normalize_status!(normalized)
|
|
42
|
+
normalize_error!(normalized)
|
|
43
|
+
normalize_effects!(normalized)
|
|
44
|
+
normalized
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build(plan_digest:, branch:, effects:, **result)
|
|
50
|
+
values = {
|
|
51
|
+
plan_digest:,
|
|
52
|
+
branch_digest: branch.digest,
|
|
53
|
+
ordinal: branch.ordinal,
|
|
54
|
+
branch_key: branch.key,
|
|
55
|
+
agent: branch.agent,
|
|
56
|
+
status: result.fetch(:status),
|
|
57
|
+
output: normalize_output(result.fetch(:output)),
|
|
58
|
+
error: result.fetch(:error),
|
|
59
|
+
effects:
|
|
60
|
+
}
|
|
61
|
+
new(values.merge(digest: PayloadDigest.call(serializable(values))))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def normalize_status!(attributes)
|
|
65
|
+
attributes[:status] = Enums.normalize(:status, attributes[:status])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def normalize_error!(attributes)
|
|
69
|
+
error = attributes[:error]
|
|
70
|
+
attributes[:error] = Error.deserialize(error) if error && !error.is_a?(Error)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def normalize_effects!(attributes)
|
|
74
|
+
effects = attributes[:effects]
|
|
75
|
+
attributes[:effects] = Effects.deserialize(effects) unless effects.is_a?(Effects)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_output(value)
|
|
79
|
+
MessageValueNormalizer.new(value, label: "composite branch output").call
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def serializable(values)
|
|
83
|
+
values.merge(error: values[:error]&.to_h, effects: values.fetch(:effects).to_h)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def initialize(attributes)
|
|
88
|
+
owned = self.class.normalize_attributes(attributes)
|
|
89
|
+
owned[:output] = self.class.send(:normalize_output, owned[:output])
|
|
90
|
+
super(owned)
|
|
91
|
+
validate_shape!
|
|
92
|
+
expected = PayloadDigest.call(self.class.send(:serializable, to_h.except(:digest)))
|
|
93
|
+
raise ArgumentError, "composite branch outcome digest does not match" unless digest == expected
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def succeeded? = status == :succeeded
|
|
97
|
+
def failed? = status == :failed
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def validate_shape!
|
|
102
|
+
valid = succeeded? ? error.nil? : error && output.nil?
|
|
103
|
+
raise ArgumentError, "composite branch outcome fields do not match status" unless valid
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -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
|