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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +264 -0
- data/README.md +191 -4
- data/docs/CONFIGURATION.md +4 -1
- data/docs/PATTERNS.md +7 -0
- data/docs/PERSISTENCE.md +347 -25
- data/lib/smith/agent/lifecycle.rb +19 -10
- data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
- data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
- data/lib/smith/agent/registry.rb +8 -0
- data/lib/smith/agent.rb +37 -2
- data/lib/smith/budget/amount_contract.rb +67 -0
- data/lib/smith/budget/amount_representation.rb +43 -0
- data/lib/smith/budget/decimal_context.rb +18 -0
- data/lib/smith/budget/ledger.rb +110 -18
- data/lib/smith/budget/ledger_publication.rb +60 -0
- data/lib/smith/budget/ledger_state.rb +21 -0
- data/lib/smith/budget/ledger_state_transition.rb +72 -0
- data/lib/smith/budget/public_state_validator.rb +25 -0
- data/lib/smith/budget/reservation.rb +20 -0
- data/lib/smith/budget/reservation_contract.rb +34 -0
- data/lib/smith/context/observation_masking.rb +3 -1
- data/lib/smith/context/state_injection.rb +1 -1
- data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -4
- data/lib/smith/error.rb +5 -0
- data/lib/smith/errors.rb +10 -0
- data/lib/smith/exponential_backoff.rb +128 -0
- data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
- data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
- data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +7 -4
- data/lib/smith/persistence_adapters/cache_store.rb +1 -0
- data/lib/smith/persistence_adapters/memory.rb +16 -1
- data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
- data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
- data/lib/smith/persistence_adapters/redis_store.rb +28 -11
- data/lib/smith/persistence_adapters/retry.rb +22 -10
- data/lib/smith/persistence_adapters.rb +21 -18
- data/lib/smith/persistence_payload_conflict.rb +12 -0
- data/lib/smith/tool/budget_enforcement.rb +2 -2
- data/lib/smith/types.rb +1 -0
- data/lib/smith/version.rb +2 -1
- data/lib/smith/workflow/branch_env.rb +1 -1
- data/lib/smith/workflow/budget_integration.rb +21 -11
- 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/definition_identity/class_methods.rb +48 -0
- data/lib/smith/workflow/definition_identity.rb +25 -0
- data/lib/smith/workflow/deterministic_step.rb +2 -2
- data/lib/smith/workflow/dsl.rb +73 -5
- data/lib/smith/workflow/durability.rb +1 -1
- data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
- data/lib/smith/workflow/execution.rb +28 -30
- data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
- data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
- data/lib/smith/workflow/fanout_execution.rb +20 -19
- data/lib/smith/workflow/graph/diagnostic.rb +27 -1
- data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
- data/lib/smith/workflow/graph/execution_successors.rb +65 -0
- data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
- data/lib/smith/workflow/graph/metrics.rb +1 -3
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
- data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
- data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
- data/lib/smith/workflow/graph/reachability.rb +19 -31
- data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
- data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
- data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
- data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/transition_contract.rb +47 -0
- data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
- data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
- data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
- data/lib/smith/workflow/graph/validator.rb +1 -1
- data/lib/smith/workflow/graph.rb +62 -4
- data/lib/smith/workflow/guardrail_integration.rb +23 -9
- data/lib/smith/workflow/identifier.rb +24 -0
- data/lib/smith/workflow/message_admission.rb +40 -0
- data/lib/smith/workflow/message_admission_boundary.rb +22 -0
- data/lib/smith/workflow/message_batch.rb +60 -0
- data/lib/smith/workflow/message_value_normalizer.rb +134 -0
- data/lib/smith/workflow/nested_execution.rb +10 -0
- data/lib/smith/workflow/orchestrator_worker.rb +3 -7
- data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
- data/lib/smith/workflow/parallel/execution_context.rb +113 -0
- data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
- data/lib/smith/workflow/parallel/root_execution.rb +91 -0
- data/lib/smith/workflow/parallel.rb +19 -22
- data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
- data/lib/smith/workflow/parallel_execution.rb +46 -14
- data/lib/smith/workflow/persistence.rb +37 -8
- data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
- data/lib/smith/workflow/prepared_step.rb +122 -0
- data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
- data/lib/smith/workflow/prepared_step_execution_authorization.rb +101 -0
- data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
- data/lib/smith/workflow/prepared_step_execution_scope.rb +111 -0
- data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
- data/lib/smith/workflow/process_local.rb +33 -0
- data/lib/smith/workflow/retry_execution.rb +20 -11
- data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
- data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
- 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/definition_boundary.rb +19 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +20 -64
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +109 -0
- data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +109 -0
- data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
- data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +77 -0
- data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
- data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
- data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
- data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
- data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
- data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
- data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
- data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
- data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
- data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +66 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
- data/lib/smith/workflow/split_step_persistence.rb +50 -0
- data/lib/smith/workflow/step_completion.rb +31 -0
- data/lib/smith/workflow/step_context.rb +46 -0
- data/lib/smith/workflow/string_snapshot.rb +25 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
- data/lib/smith/workflow/transition.rb +137 -133
- data/lib/smith/workflow/transition_actionability.rb +22 -0
- data/lib/smith/workflow.rb +64 -18
- data/lib/smith.rb +44 -2
- data/sig/smith.rbs +1 -0
- 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
|