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,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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module DefinitionIdentity
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def inherited(subclass)
|
|
8
|
+
super
|
|
9
|
+
digest = definition_identity_mutex.synchronize { @definition_digest }
|
|
10
|
+
subclass.instance_variable_set(:@definition_digest, digest)
|
|
11
|
+
subclass.instance_variable_set(:@definition_identity_mutex, Mutex.new)
|
|
12
|
+
subclass.instance_variable_set(:@definition_identity_sealed, false)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def definition_digest(value = nil)
|
|
16
|
+
return definition_identity_mutex.synchronize { @definition_digest } if value.nil?
|
|
17
|
+
|
|
18
|
+
unless Smith::Types::Sha256Hex.valid?(value)
|
|
19
|
+
raise ArgumentError, "definition_digest must be a lowercase SHA-256 hex digest"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
digest = value.dup.freeze
|
|
23
|
+
definition_identity_mutex.synchronize do
|
|
24
|
+
return @definition_digest if @definition_digest == digest
|
|
25
|
+
if @definition_identity_sealed || frozen?
|
|
26
|
+
raise ArgumentError, "definition_digest is sealed for this workflow class"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@definition_digest = digest
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def seal_definition_identity!
|
|
36
|
+
definition_identity_mutex.synchronize do
|
|
37
|
+
@definition_identity_sealed = true unless frozen?
|
|
38
|
+
@definition_digest
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def definition_identity_mutex
|
|
43
|
+
@definition_identity_mutex ||= Mutex.new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "definition_identity/class_methods"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module DefinitionIdentity
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.instance_variable_set(:@definition_identity_mutex, Mutex.new)
|
|
10
|
+
base.instance_variable_set(:@definition_identity_sealed, false)
|
|
11
|
+
base.extend(ClassMethods)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def effective_definition_digest
|
|
17
|
+
if instance_variable_defined?(:@split_step_definition_digest)
|
|
18
|
+
@split_step_definition_digest
|
|
19
|
+
else
|
|
20
|
+
self.class.definition_digest
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -23,8 +23,8 @@ module Smith
|
|
|
23
23
|
def last_output
|
|
24
24
|
return @last_output if defined?(@last_output)
|
|
25
25
|
|
|
26
|
-
msg = session_messages.reverse.find { |m| m[:role] == :assistant || m[
|
|
27
|
-
@last_output = msg
|
|
26
|
+
msg = session_messages.reverse.find { |m| m[:role] == :assistant || m["role"] == "assistant" }
|
|
27
|
+
@last_output = msg && (msg[:content] || msg["content"])
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
alias_method :output, :last_output
|