smith-agents 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +63 -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/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,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../types"
|
|
4
|
+
require_relative "../../version"
|
|
5
|
+
require_relative "../prepared_step_dispatch"
|
|
6
|
+
require_relative "branch"
|
|
7
|
+
require_relative "enums"
|
|
8
|
+
require_relative "payload"
|
|
9
|
+
require_relative "payload_digest"
|
|
10
|
+
require_relative "plan_integrity"
|
|
11
|
+
|
|
12
|
+
module Smith
|
|
13
|
+
class Workflow
|
|
14
|
+
module Composite
|
|
15
|
+
class Plan < Payload
|
|
16
|
+
VERSION = 1
|
|
17
|
+
MAX_BRANCHES = 10_000
|
|
18
|
+
RESUME_POLICY = :incomplete_only
|
|
19
|
+
FAILURE_POLICY = :host_committed_primary
|
|
20
|
+
REDUCTION_POLICY = :ordered_all_success
|
|
21
|
+
RETRY_POLICY = :none
|
|
22
|
+
ENUM_ATTRIBUTES = %i[kind resume_policy failure_policy reduction_policy retry_policy].freeze
|
|
23
|
+
private_constant :ENUM_ATTRIBUTES
|
|
24
|
+
OwnedString = Types::String.constructor { |value| value.is_a?(String) ? value.dup.freeze : value }
|
|
25
|
+
private_constant :OwnedString
|
|
26
|
+
|
|
27
|
+
attribute :version, Types::Integer.enum(VERSION)
|
|
28
|
+
attribute :execution_semantics_version, OwnedString.constrained(min_size: 1, max_size: 32)
|
|
29
|
+
attribute :dispatch, Types.Instance(PreparedStepDispatch)
|
|
30
|
+
attribute :kind, Types::Symbol.enum(:parallel, :fanout)
|
|
31
|
+
attribute :transition, OwnedString.constrained(min_size: 1, max_size: 256)
|
|
32
|
+
attribute :from, OwnedString.constrained(min_size: 1, max_size: 256)
|
|
33
|
+
attribute :execution_namespace, OwnedString.constrained(format: PreparedStep::UUID_PATTERN)
|
|
34
|
+
attribute :branches, Types::Array.of(Types.Instance(Branch))
|
|
35
|
+
attribute :input_digest, OwnedString.constrained(format: PreparedStep::DIGEST_PATTERN)
|
|
36
|
+
attribute :budget_state_digest, OwnedString.constrained(format: PreparedStep::DIGEST_PATTERN)
|
|
37
|
+
attribute :resume_policy, Types::Symbol.enum(RESUME_POLICY)
|
|
38
|
+
attribute :failure_policy, Types::Symbol.enum(FAILURE_POLICY)
|
|
39
|
+
attribute :reduction_policy, Types::Symbol.enum(REDUCTION_POLICY)
|
|
40
|
+
attribute :retry_policy, Types::Symbol.enum(RETRY_POLICY)
|
|
41
|
+
attribute :plan_digest, OwnedString.constrained(format: PreparedStep::DIGEST_PATTERN)
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
def build(**attributes)
|
|
45
|
+
values = {
|
|
46
|
+
version: VERSION,
|
|
47
|
+
execution_semantics_version: Smith::EXECUTION_SEMANTICS_VERSION,
|
|
48
|
+
dispatch: attributes.fetch(:dispatch),
|
|
49
|
+
kind: attributes.fetch(:kind),
|
|
50
|
+
transition: attributes.fetch(:transition).to_s,
|
|
51
|
+
from: attributes.fetch(:from).to_s,
|
|
52
|
+
execution_namespace: attributes.fetch(:execution_namespace),
|
|
53
|
+
branches: attributes.fetch(:branches),
|
|
54
|
+
input_digest: attributes.fetch(:input_digest),
|
|
55
|
+
budget_state_digest: attributes.fetch(:budget_state_digest),
|
|
56
|
+
resume_policy: RESUME_POLICY,
|
|
57
|
+
failure_policy: FAILURE_POLICY,
|
|
58
|
+
reduction_policy: REDUCTION_POLICY,
|
|
59
|
+
retry_policy: RETRY_POLICY
|
|
60
|
+
}
|
|
61
|
+
new(values.merge(plan_digest: PayloadDigest.call(serializable(values))))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def normalize_attributes(attributes)
|
|
65
|
+
normalized = super
|
|
66
|
+
normalize_dispatch!(normalized)
|
|
67
|
+
normalize_branches!(normalized)
|
|
68
|
+
normalize_policies!(normalized)
|
|
69
|
+
normalized
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def preflight_attributes!(attributes)
|
|
73
|
+
branches = raw_attribute(attributes, :branches)
|
|
74
|
+
validate_branch_count!(branches) if branches.is_a?(Array)
|
|
75
|
+
attributes
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def serializable(values)
|
|
79
|
+
values.merge(
|
|
80
|
+
dispatch: values.fetch(:dispatch).to_h,
|
|
81
|
+
branches: values.fetch(:branches).map(&:to_h)
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_branch_count!(branches_or_count)
|
|
86
|
+
count = if branches_or_count.is_a?(Array)
|
|
87
|
+
Array.instance_method(:length).bind_call(branches_or_count)
|
|
88
|
+
else
|
|
89
|
+
branches_or_count
|
|
90
|
+
end
|
|
91
|
+
return if count&.between?(1, MAX_BRANCHES)
|
|
92
|
+
|
|
93
|
+
raise ArgumentError, "composite plan branch count is outside the transport limit"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def normalize_dispatch!(attributes)
|
|
99
|
+
dispatch = attributes[:dispatch]
|
|
100
|
+
attributes[:dispatch] = PreparedStepDispatch.deserialize(dispatch) unless
|
|
101
|
+
dispatch.is_a?(PreparedStepDispatch)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def normalize_branches!(attributes)
|
|
105
|
+
branches = attributes[:branches]
|
|
106
|
+
validate_branch_count!(branches)
|
|
107
|
+
normalized = []
|
|
108
|
+
Array.instance_method(:each).bind_call(branches) do |branch|
|
|
109
|
+
normalized << (branch.is_a?(Branch) ? branch : Branch.deserialize(branch))
|
|
110
|
+
end
|
|
111
|
+
attributes[:branches] = normalized.freeze
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def normalize_policies!(attributes)
|
|
115
|
+
ENUM_ATTRIBUTES.each { |key| attributes[key] = Enums.normalize(key, attributes[key]) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def initialize(attributes)
|
|
120
|
+
self.class.preflight_attributes!(attributes)
|
|
121
|
+
owned = self.class.normalize_attributes(attributes)
|
|
122
|
+
super(owned)
|
|
123
|
+
PlanIntegrity.new(self).call
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def execution_for(branch) = BranchExecution.build(plan: self, branch:)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "payload_digest"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
module Composite
|
|
10
|
+
class PlanIntegrity
|
|
11
|
+
extend Dry::Initializer
|
|
12
|
+
|
|
13
|
+
param :plan
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
validate_execution_semantics!
|
|
17
|
+
validate_branches!
|
|
18
|
+
validate_digest!
|
|
19
|
+
plan
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def validate_execution_semantics!
|
|
25
|
+
return if plan.execution_semantics_version == Smith::EXECUTION_SEMANTICS_VERSION
|
|
26
|
+
|
|
27
|
+
raise ArgumentError, "composite plan execution semantics do not match"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate_branches!
|
|
31
|
+
keys = {}
|
|
32
|
+
plan.branches.each_with_index do |branch, ordinal|
|
|
33
|
+
raise ArgumentError, "composite plan ordinals must be contiguous" unless branch.ordinal == ordinal
|
|
34
|
+
raise ArgumentError, "composite plan branch keys must be unique" if keys.key?(branch.key)
|
|
35
|
+
|
|
36
|
+
keys[branch.key] = true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_digest!
|
|
41
|
+
attributes = plan.to_h.except(:plan_digest).merge(
|
|
42
|
+
dispatch: plan.dispatch.to_h,
|
|
43
|
+
branches: plan.branches.map(&:to_h)
|
|
44
|
+
)
|
|
45
|
+
return if plan.plan_digest == PayloadDigest.call(attributes)
|
|
46
|
+
|
|
47
|
+
raise ArgumentError, "composite plan digest does not match"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "branch"
|
|
6
|
+
require_relative "plan"
|
|
7
|
+
|
|
8
|
+
module Smith
|
|
9
|
+
class Workflow
|
|
10
|
+
module Composite
|
|
11
|
+
class Planner
|
|
12
|
+
extend Dry::Initializer
|
|
13
|
+
|
|
14
|
+
option :dispatch
|
|
15
|
+
option :kind
|
|
16
|
+
option :transition
|
|
17
|
+
option :from
|
|
18
|
+
option :execution_namespace
|
|
19
|
+
option :branch_specs
|
|
20
|
+
option :input_digest
|
|
21
|
+
option :budget_state_digest
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
Plan.validate_branch_count!(branch_specs)
|
|
25
|
+
Plan.build(
|
|
26
|
+
dispatch:,
|
|
27
|
+
kind:,
|
|
28
|
+
transition:,
|
|
29
|
+
from:,
|
|
30
|
+
execution_namespace:,
|
|
31
|
+
branches: build_branches,
|
|
32
|
+
input_digest:,
|
|
33
|
+
budget_state_digest:
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_branches
|
|
40
|
+
branch_specs.each_with_index.map do |spec, ordinal|
|
|
41
|
+
Branch.build(
|
|
42
|
+
ordinal:,
|
|
43
|
+
key: spec.fetch(:key),
|
|
44
|
+
agent: spec.fetch(:agent),
|
|
45
|
+
binding_identity: spec.fetch(:binding_identity),
|
|
46
|
+
budget: spec.fetch(:budget, {})
|
|
47
|
+
)
|
|
48
|
+
end.freeze
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../types"
|
|
4
|
+
require_relative "input"
|
|
5
|
+
require_relative "plan"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
module Composite
|
|
10
|
+
class Preparation < Dry::Struct
|
|
11
|
+
attribute :plan, Types.Instance(Plan)
|
|
12
|
+
attribute :input, Types.Instance(Input)
|
|
13
|
+
|
|
14
|
+
def initialize(attributes)
|
|
15
|
+
super
|
|
16
|
+
unless plan.input_digest == input.digest
|
|
17
|
+
raise ArgumentError,
|
|
18
|
+
"composite preparation input does not match plan"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
self.attributes.freeze
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "branch_outcome"
|
|
6
|
+
require_relative "budget_math"
|
|
7
|
+
require_relative "effects"
|
|
8
|
+
require_relative "outcome_accumulator"
|
|
9
|
+
require_relative "plan"
|
|
10
|
+
require_relative "reduction"
|
|
11
|
+
|
|
12
|
+
module Smith
|
|
13
|
+
class Workflow
|
|
14
|
+
module Composite
|
|
15
|
+
class Reducer
|
|
16
|
+
extend Dry::Initializer
|
|
17
|
+
|
|
18
|
+
option :plan
|
|
19
|
+
option :outcomes
|
|
20
|
+
option :primary_failure, optional: true
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
outcome_set = OutcomeAccumulator.new(plan:, outcomes:).call
|
|
24
|
+
ordered = outcome_set.ordered
|
|
25
|
+
validate_effects!(ordered)
|
|
26
|
+
effects = merged_effects(ordered)
|
|
27
|
+
failure_seen, selected_failure = failure_state(ordered)
|
|
28
|
+
return successful_reduction(outcome_set.output, effects) unless failure_seen
|
|
29
|
+
|
|
30
|
+
failed_reduction(selected_failure, effects)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate_effects!(ordered)
|
|
36
|
+
usage_ids = {}
|
|
37
|
+
ordered.each { validate_outcome_effects!(_1, usage_ids) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_outcome_effects!(outcome, usage_ids)
|
|
41
|
+
branch = plan.branches.fetch(outcome.ordinal)
|
|
42
|
+
validate_budget!(outcome.effects, branch.budget)
|
|
43
|
+
validate_usage!(outcome.effects.usage_entries, branch, usage_ids)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_usage!(entries, branch, usage_ids)
|
|
47
|
+
entries.each { validate_usage_entry!(_1, branch, usage_ids) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def validate_usage_entry!(entry, branch, usage_ids)
|
|
51
|
+
unless entry.fetch("agent_name") == branch.agent
|
|
52
|
+
raise ArgumentError, "composite usage entry does not match its branch agent"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
usage_id = entry.fetch("usage_id")
|
|
56
|
+
raise ArgumentError, "composite usage entry is duplicated" if usage_ids.key?(usage_id)
|
|
57
|
+
|
|
58
|
+
usage_ids[usage_id] = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_budget!(effects, envelope)
|
|
62
|
+
consumed = effects.budget_consumed
|
|
63
|
+
validate_budget_dimensions!(consumed, envelope)
|
|
64
|
+
envelope.each { |dimension, limit| validate_budget_dimension!(dimension, limit, consumed, effects) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_budget_dimensions!(consumed, envelope)
|
|
68
|
+
return if (consumed.keys - envelope.keys).empty?
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, "composite branch consumed an undeclared budget dimension"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_budget_dimension!(dimension, limit, consumed, effects)
|
|
74
|
+
amount = consumed.fetch(dimension, 0)
|
|
75
|
+
unless amount.is_a?(Numeric) && amount.finite? && amount >= 0 && amount <= limit
|
|
76
|
+
raise ArgumentError, "composite branch budget consumption exceeds its envelope"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
expected = expected_consumption(dimension, effects)
|
|
80
|
+
return if decimal_equal?(amount, expected)
|
|
81
|
+
|
|
82
|
+
raise ArgumentError, "composite branch budget consumption does not match recorded usage"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def expected_consumption(dimension, effects)
|
|
86
|
+
return effects.total_tokens if %w[total_tokens token_limit].include?(dimension)
|
|
87
|
+
return effects.total_cost if dimension == "total_cost"
|
|
88
|
+
|
|
89
|
+
0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def decimal_equal?(left, right)
|
|
93
|
+
Budget::DecimalContext.call { BigDecimal(left.to_s) == BigDecimal(right.to_s) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def merged_effects(ordered)
|
|
97
|
+
Effects.new(
|
|
98
|
+
usage_entries: ordered.flat_map { _1.effects.usage_entries },
|
|
99
|
+
tool_results: ordered.flat_map { _1.effects.tool_results },
|
|
100
|
+
budget_consumed: BudgetMath.sum(ordered.map { _1.effects.budget_consumed })
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failure_state(ordered)
|
|
105
|
+
expected_key = primary_failure&.to_s
|
|
106
|
+
failure_seen = false
|
|
107
|
+
selected_failure = nil
|
|
108
|
+
ordered.each do |outcome|
|
|
109
|
+
next unless outcome.failed?
|
|
110
|
+
|
|
111
|
+
failure_seen = true
|
|
112
|
+
selected_failure = outcome if outcome.branch_key == expected_key
|
|
113
|
+
end
|
|
114
|
+
[failure_seen, selected_failure]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def successful_reduction(output, effects)
|
|
118
|
+
raise ArgumentError, "primary failure must be absent for a successful composite" if primary_failure
|
|
119
|
+
|
|
120
|
+
Reduction.new(status: :succeeded, output:, error: nil,
|
|
121
|
+
failed_branch_key: nil, effects:)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def failed_reduction(failure, effects)
|
|
125
|
+
raise ArgumentError, "primary failure must identify a failed branch" unless failure
|
|
126
|
+
|
|
127
|
+
Reduction.new(status: :failed, output: nil, error: failure.error,
|
|
128
|
+
failed_branch_key: failure.branch_key, effects:)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../types"
|
|
4
|
+
require_relative "effects"
|
|
5
|
+
require_relative "error"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
module Composite
|
|
10
|
+
class Reduction < Dry::Struct
|
|
11
|
+
attribute :status, Types::Symbol.enum(:succeeded, :failed)
|
|
12
|
+
attribute? :output, Types::Any.optional
|
|
13
|
+
attribute? :error, Types.Instance(Error).optional
|
|
14
|
+
attribute? :failed_branch_key, Types::String.optional
|
|
15
|
+
attribute :effects, Types.Instance(Effects)
|
|
16
|
+
|
|
17
|
+
def initialize(attributes)
|
|
18
|
+
super
|
|
19
|
+
valid = status == :succeeded ? error.nil? && failed_branch_key.nil? : error && failed_branch_key
|
|
20
|
+
raise ArgumentError, "composite reduction fields do not match status" unless valid
|
|
21
|
+
|
|
22
|
+
self.attributes.freeze
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def succeeded? = status == :succeeded
|
|
27
|
+
def failed? = status == :failed
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "../../errors"
|
|
6
|
+
require_relative "../string_snapshot"
|
|
7
|
+
|
|
8
|
+
module Smith
|
|
9
|
+
class Workflow
|
|
10
|
+
module Composite
|
|
11
|
+
class ValueBudget
|
|
12
|
+
extend Dry::Initializer
|
|
13
|
+
|
|
14
|
+
ARRAY_EACH = Array.instance_method(:each)
|
|
15
|
+
HASH_EACH_PAIR = Hash.instance_method(:each_pair)
|
|
16
|
+
private_constant :ARRAY_EACH, :HASH_EACH_PAIR
|
|
17
|
+
|
|
18
|
+
option :max_bytes
|
|
19
|
+
option :max_nodes
|
|
20
|
+
option :max_depth
|
|
21
|
+
option :label
|
|
22
|
+
|
|
23
|
+
def initialize(...)
|
|
24
|
+
super
|
|
25
|
+
@bytes = 0
|
|
26
|
+
@nodes = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add(value, depth: 0)
|
|
30
|
+
pending = [[value, depth]]
|
|
31
|
+
until pending.empty?
|
|
32
|
+
item, item_depth = pending.pop
|
|
33
|
+
visit!(item, item_depth, pending)
|
|
34
|
+
end
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def visit!(item, depth, pending)
|
|
41
|
+
validate_visit!(depth)
|
|
42
|
+
dispatch_value(item, depth, pending)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def dispatch_value(item, depth, pending)
|
|
46
|
+
case item
|
|
47
|
+
when Hash then enqueue_hash(item, depth, pending)
|
|
48
|
+
when Array then enqueue_array(item, depth, pending)
|
|
49
|
+
when String, Symbol then add_bytes!(item.to_s)
|
|
50
|
+
when Float then validate_float!(item)
|
|
51
|
+
when Integer, true, false, nil then nil
|
|
52
|
+
else raise WorkflowError, "#{label} contains unsupported value #{item.class}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_visit!(depth)
|
|
57
|
+
raise WorkflowError, "#{label} exceeds maximum depth #{max_depth}" if depth > max_depth
|
|
58
|
+
|
|
59
|
+
@nodes += 1
|
|
60
|
+
raise WorkflowError, "#{label} exceeds maximum size #{max_nodes}" if @nodes > max_nodes
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def enqueue_hash(hash, depth, pending)
|
|
64
|
+
HASH_EACH_PAIR.bind_call(hash) do |key, value|
|
|
65
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
66
|
+
raise WorkflowError, "#{label} contains unsupported Hash key #{key.class}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
pending << [value, depth + 1] << [key, depth + 1]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def enqueue_array(array, depth, pending)
|
|
74
|
+
ARRAY_EACH.bind_call(array) { |value| pending << [value, depth + 1] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def add_bytes!(string)
|
|
78
|
+
@bytes += StringSnapshot.bytesize(string)
|
|
79
|
+
raise WorkflowError, "#{label} exceeds maximum bytes #{max_bytes}" if @bytes > max_bytes
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_float!(float)
|
|
83
|
+
return if float.finite?
|
|
84
|
+
|
|
85
|
+
raise WorkflowError, "#{label} contains a non-finite Float"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "prepared_step_execution_authorization"
|
|
4
|
+
require_relative "composite/branch_execution"
|
|
5
|
+
require_relative "composite/input"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
class CompositeBranchExecutionAuthorization < PreparedStepExecutionAuthorization
|
|
10
|
+
attr_reader :execution_digest, :plan_digest, :input_digest, :branch_digest,
|
|
11
|
+
:branch_ordinal, :execution_namespace
|
|
12
|
+
|
|
13
|
+
def initialize(execution:, input:, **attributes)
|
|
14
|
+
validate_payloads!(execution, input)
|
|
15
|
+
@execution_digest = execution.digest.dup.freeze
|
|
16
|
+
@plan_digest = execution.plan_digest.dup.freeze
|
|
17
|
+
@input_digest = input.digest.dup.freeze
|
|
18
|
+
assign_branch_identity(execution.branch)
|
|
19
|
+
@execution_namespace = execution.execution_namespace.dup.freeze
|
|
20
|
+
super(**attributes)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_payloads!(execution, input)
|
|
26
|
+
return if execution.is_a?(Composite::BranchExecution) && input.is_a?(Composite::Input)
|
|
27
|
+
|
|
28
|
+
raise ArgumentError, "composite branch authorization requires typed execution and input"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def assign_branch_identity(branch)
|
|
32
|
+
@branch_digest = branch.digest.dup.freeze
|
|
33
|
+
@branch_ordinal = branch.ordinal
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def verify_composite_branch!(execution, input)
|
|
37
|
+
validate_payloads!(execution, input)
|
|
38
|
+
expected = [
|
|
39
|
+
execution_digest, plan_digest, branch_digest, branch_ordinal,
|
|
40
|
+
execution_namespace, input_digest
|
|
41
|
+
]
|
|
42
|
+
actual = [
|
|
43
|
+
execution.digest, execution.plan_digest, execution.branch.digest,
|
|
44
|
+
execution.branch.ordinal, execution.execution_namespace, input.digest
|
|
45
|
+
]
|
|
46
|
+
return if actual == expected
|
|
47
|
+
|
|
48
|
+
raise WorkflowError, "composite branch authorization does not match execution"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
|
|
5
|
+
require_relative "thread_context_snapshot"
|
|
6
|
+
|
|
5
7
|
module Smith
|
|
6
8
|
class Workflow
|
|
7
9
|
module DeadlineEnforcement
|
|
@@ -19,17 +21,32 @@ module Smith
|
|
|
19
21
|
[wall_clock_deadline, call_dl].compact.min
|
|
20
22
|
end
|
|
21
23
|
|
|
22
|
-
def with_agent_context(agent_class)
|
|
24
|
+
def with_agent_context(agent_class, &block)
|
|
23
25
|
saved_deadline = Tool.current_deadline
|
|
24
26
|
saved_call_ledger = Thread.current[:smith_call_ledger]
|
|
27
|
+
snapshot = ThreadContextSnapshot.new(
|
|
28
|
+
tool_attributes: %i[current_deadline current_tool_call_allowance],
|
|
29
|
+
thread_keys: %i[smith_call_deadline smith_call_ledger],
|
|
30
|
+
scoped_artifacts: false
|
|
31
|
+
)
|
|
32
|
+
snapshot.around do
|
|
33
|
+
apply_agent_context(agent_class)
|
|
34
|
+
Thread.handle_interrupt(Object => :immediate, &block)
|
|
35
|
+
ensure
|
|
36
|
+
restore_agent_context(saved_deadline, saved_call_ledger)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply_agent_context(agent_class)
|
|
25
41
|
apply_agent_deadline(agent_class)
|
|
26
42
|
narrow_tool_deadline!
|
|
27
43
|
apply_agent_tool_calls(agent_class)
|
|
28
44
|
apply_agent_call_ledger(agent_class)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def restore_agent_context(deadline, call_ledger)
|
|
48
|
+
Tool.current_deadline = deadline
|
|
49
|
+
Thread.current[:smith_call_ledger] = call_ledger
|
|
33
50
|
clear_agent_deadline
|
|
34
51
|
clear_agent_tool_calls
|
|
35
52
|
end
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "agent_result"
|
|
4
4
|
require_relative "execution_binding_resolution"
|
|
5
|
+
require_relative "prepared_branch_execution"
|
|
5
6
|
require_relative "step_completion"
|
|
7
|
+
require_relative "step_context"
|
|
6
8
|
|
|
7
9
|
module Smith
|
|
8
10
|
class Workflow
|
|
9
11
|
module Execution
|
|
10
12
|
include ExecutionBindingResolution
|
|
13
|
+
include PreparedBranchExecution
|
|
11
14
|
include StepCompletion
|
|
15
|
+
include StepContext
|
|
12
16
|
include Agent::Lifecycle
|
|
13
17
|
include NestedExecution
|
|
14
18
|
include EvaluatorOptimizer
|
|
@@ -21,14 +25,12 @@ module Smith
|
|
|
21
25
|
private
|
|
22
26
|
|
|
23
27
|
def execute_step(transition)
|
|
24
|
-
|
|
28
|
+
with_step_context(transition) { execute_step_body(transition) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def execute_step_body(transition)
|
|
25
32
|
output = with_scoped_artifacts { run_with_retry_policy(transition) }
|
|
26
33
|
StepCompletion.instance_method(:complete_step).bind_call(self, transition, output)
|
|
27
|
-
rescue StandardError => e
|
|
28
|
-
@outcome = nil
|
|
29
|
-
GuardrailIntegration.instance_method(:handle_step_failure).bind_call(self, transition, e)
|
|
30
|
-
ensure
|
|
31
|
-
teardown_step_context
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def setup_step_context
|
|
@@ -37,16 +39,16 @@ module Smith
|
|
|
37
39
|
Tool.current_tool_result_collector = tool_result_collector
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
def teardown_step_context
|
|
41
|
-
Tool.current_guardrails = nil
|
|
42
|
-
Tool.current_deadline = nil
|
|
43
|
-
Tool.current_ledger = nil
|
|
44
|
-
Tool.current_tool_result_collector = nil
|
|
45
|
-
Smith.scoped_artifacts = nil
|
|
46
|
-
end
|
|
47
|
-
|
|
48
42
|
def run_guarded_step(transition)
|
|
43
|
+
return apply_composite_reduction!(transition) if @composite_reduction
|
|
44
|
+
|
|
49
45
|
@resolved_parallel_branch_count = preflight_branch_count(transition)
|
|
46
|
+
run_standard_guarded_step(transition)
|
|
47
|
+
ensure
|
|
48
|
+
@resolved_parallel_branch_count = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run_standard_guarded_step(transition)
|
|
50
52
|
return dispatch_step(transition) if transition.deterministic?
|
|
51
53
|
return run_guarded_fanout_step(transition) if transition.fanout?
|
|
52
54
|
|
|
@@ -62,8 +64,6 @@ module Smith
|
|
|
62
64
|
validate_data_volume!(output, agent_class)
|
|
63
65
|
run_output_guardrails(output, agent_class)
|
|
64
66
|
resolve_router_output(transition, output)
|
|
65
|
-
ensure
|
|
66
|
-
@resolved_parallel_branch_count = nil
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def preflight_branch_count(transition)
|