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,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../composite/branch_outcome"
|
|
4
|
+
require_relative "../composite/error_evidence"
|
|
5
|
+
|
|
6
|
+
module Smith
|
|
7
|
+
class Workflow
|
|
8
|
+
module SplitStepPersistence
|
|
9
|
+
module CompositeBranchOutcome
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def composite_branch_outcome(execution, output, error, effects)
|
|
13
|
+
return failed_composite_branch_outcome(execution, error, effects) if error
|
|
14
|
+
|
|
15
|
+
Composite::BranchOutcome.succeeded(
|
|
16
|
+
plan_digest: execution.plan_digest,
|
|
17
|
+
branch: execution.branch,
|
|
18
|
+
output: output.fetch(:output),
|
|
19
|
+
effects:
|
|
20
|
+
)
|
|
21
|
+
rescue WorkflowError, ArgumentError => e
|
|
22
|
+
failed_composite_branch_outcome(execution, e, effects)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def failed_composite_branch_outcome(execution, error, effects)
|
|
26
|
+
Composite::BranchOutcome.failed(
|
|
27
|
+
plan_digest: execution.plan_digest,
|
|
28
|
+
branch: execution.branch,
|
|
29
|
+
error: Composite::ErrorEvidence.call(error),
|
|
30
|
+
effects:
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module CompositeExecution
|
|
7
|
+
def prepare_composite_step!
|
|
8
|
+
ExecutionAuthorizationIssuance
|
|
9
|
+
.instance_method(:with_prepared_step_execution_authorization)
|
|
10
|
+
.bind_call(self) do |authorization|
|
|
11
|
+
CompositePreparation
|
|
12
|
+
.instance_method(:prepare_authorized_composite_step!)
|
|
13
|
+
.bind_call(self, authorization)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def execute_prepared_composite_branch!(execution:, input:)
|
|
18
|
+
CompositeBranchAuthorization
|
|
19
|
+
.instance_method(:with_composite_branch_authorization)
|
|
20
|
+
.bind_call(self, execution, input) do |authorization|
|
|
21
|
+
CompositeBranchExecution
|
|
22
|
+
.instance_method(:execute_authorized_composite_branch!)
|
|
23
|
+
.bind_call(self, authorization, execution:, input:)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reduce_prepared_composite_step!(plan:, input:, outcomes:, primary_failure: nil)
|
|
28
|
+
ExecutionAuthorizationIssuance
|
|
29
|
+
.instance_method(:with_prepared_step_execution_authorization)
|
|
30
|
+
.bind_call(self) do |authorization|
|
|
31
|
+
CompositeReductionExecution
|
|
32
|
+
.instance_method(:reduce_authorized_composite_step!)
|
|
33
|
+
.bind_call(self, authorization, plan:, input:, outcomes:, primary_failure:)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../composite/contract"
|
|
4
|
+
require_relative "../composite/input"
|
|
5
|
+
require_relative "../composite/planner"
|
|
6
|
+
require_relative "../composite/preparation"
|
|
7
|
+
|
|
8
|
+
module Smith
|
|
9
|
+
class Workflow
|
|
10
|
+
module SplitStepPersistence
|
|
11
|
+
module CompositePreparation
|
|
12
|
+
include Composite::Contract
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def prepare_authorized_composite_step!(authorization)
|
|
17
|
+
authorization = validate_composite_authorization!(authorization)
|
|
18
|
+
transition = @split_step_transition
|
|
19
|
+
specs = composite_branch_specs(authorization, transition)
|
|
20
|
+
input = prepare_composite_input(authorization, transition, specs)
|
|
21
|
+
plan = Composite::Planner.new(
|
|
22
|
+
dispatch: authorization.dispatch_claim,
|
|
23
|
+
kind: composite_kind(transition),
|
|
24
|
+
transition: transition.name,
|
|
25
|
+
from: transition.from,
|
|
26
|
+
execution_namespace: execution_namespace,
|
|
27
|
+
branch_specs: specs,
|
|
28
|
+
input_digest: input.digest,
|
|
29
|
+
budget_state_digest: composite_budget_state_digest
|
|
30
|
+
).call
|
|
31
|
+
Composite::Preparation.new(plan:, input:)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def prepare_composite_input(authorization, transition, specs)
|
|
35
|
+
ThreadContextSnapshot.new.around do
|
|
36
|
+
run_composite_input_guardrails(authorization, transition, specs)
|
|
37
|
+
agent_messages = build_session&.prepare!
|
|
38
|
+
Composite::Input.build(agent_messages:, session_messages: snapshot_session_messages)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run_composite_input_guardrails(authorization, transition, specs)
|
|
43
|
+
if transition.parallel?
|
|
44
|
+
agent = specs.first.fetch(:agent)
|
|
45
|
+
run_input_guardrails(captured_agent(authorization, transition, agent, :agent))
|
|
46
|
+
else
|
|
47
|
+
run_workflow_input_guardrails
|
|
48
|
+
specs.each do |spec|
|
|
49
|
+
agent = spec.fetch(:agent)
|
|
50
|
+
run_agent_input_guardrails(captured_agent(authorization, transition, agent, :fanout_agent))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../composite/branch_failure"
|
|
4
|
+
require_relative "../composite/contract"
|
|
5
|
+
require_relative "../composite/effects_baseline"
|
|
6
|
+
require_relative "../composite/effects_preflight"
|
|
7
|
+
require_relative "../composite/reducer"
|
|
8
|
+
|
|
9
|
+
module Smith
|
|
10
|
+
class Workflow
|
|
11
|
+
module SplitStepPersistence
|
|
12
|
+
module CompositeReductionExecution
|
|
13
|
+
include Composite::Contract
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def reduce_authorized_composite_step!(authorization, plan:, input:, outcomes:, primary_failure: nil)
|
|
18
|
+
authorization = validate_composite_authorization!(authorization)
|
|
19
|
+
validate_composite_reduction_plan!(authorization, plan, input)
|
|
20
|
+
reduction = Composite::Reducer.new(plan:, outcomes:, primary_failure:).call
|
|
21
|
+
effects_application = prepare_composite_effects(reduction.effects)
|
|
22
|
+
@composite_plan = plan
|
|
23
|
+
@composite_input = input
|
|
24
|
+
@composite_reduction = reduction
|
|
25
|
+
@composite_effects_application = effects_application
|
|
26
|
+
@execution_namespace = plan.execution_namespace
|
|
27
|
+
Execution.instance_method(:execute_authorized_prepared_step!).bind_call(self, authorization)
|
|
28
|
+
ensure
|
|
29
|
+
@composite_plan = nil
|
|
30
|
+
@composite_input = nil
|
|
31
|
+
@composite_reduction = nil
|
|
32
|
+
@composite_effects_application = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def apply_composite_reduction!(transition)
|
|
36
|
+
reduction = @composite_reduction
|
|
37
|
+
apply_composite_effects!(@composite_effects_application)
|
|
38
|
+
restore_composite_session!
|
|
39
|
+
if reduction.failed?
|
|
40
|
+
raise Composite::BranchFailure.new(
|
|
41
|
+
branch_key: reduction.failed_branch_key,
|
|
42
|
+
error: reduction.error
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
validate_composite_output!(transition, reduction.output)
|
|
47
|
+
reduction.output
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def restore_composite_session!
|
|
51
|
+
@session_messages = snapshot_value(@composite_input.session_messages)
|
|
52
|
+
@last_prepared_input = snapshot_value(@composite_input.agent_messages)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def prepare_composite_effects(effects)
|
|
56
|
+
Composite::EffectsPreflight.new(
|
|
57
|
+
effects:,
|
|
58
|
+
baseline: composite_effect_baseline,
|
|
59
|
+
snapshotter: method(:snapshot_value)
|
|
60
|
+
).call
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def apply_composite_effects!(application)
|
|
64
|
+
Thread.handle_interrupt(Object => :never) do
|
|
65
|
+
@usage_mutex.synchronize do
|
|
66
|
+
@tool_results_mutex.synchronize do
|
|
67
|
+
validate_composite_effect_baseline!(application.baseline)
|
|
68
|
+
@usage_entries = application.usage_entries.dup
|
|
69
|
+
@total_tokens = application.total_tokens
|
|
70
|
+
@total_cost = application.total_cost
|
|
71
|
+
@tool_results = application.tool_results.dup
|
|
72
|
+
@ledger = application.ledger
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def composite_effect_baseline
|
|
79
|
+
@usage_mutex.synchronize do
|
|
80
|
+
@tool_results_mutex.synchronize do
|
|
81
|
+
build_composite_effect_baseline
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_composite_effect_baseline
|
|
87
|
+
ledger = @ledger
|
|
88
|
+
Composite::EffectsBaseline.new(
|
|
89
|
+
usage_entries: @usage_entries.dup,
|
|
90
|
+
tool_results: snapshot_value(@tool_results),
|
|
91
|
+
total_tokens: @total_tokens,
|
|
92
|
+
total_cost: @total_cost,
|
|
93
|
+
ledger:,
|
|
94
|
+
budget_consumed: ledger ? ledger.consumed : {}
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate_composite_effect_baseline!(expected)
|
|
99
|
+
current = build_composite_effect_baseline
|
|
100
|
+
return if current == expected
|
|
101
|
+
|
|
102
|
+
raise WorkflowError, "composite effect baseline changed before application"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_composite_output!(transition, output)
|
|
106
|
+
if @composite_plan.kind == :parallel
|
|
107
|
+
agent = captured_agent(
|
|
108
|
+
@split_step_active_execution_authorization,
|
|
109
|
+
transition,
|
|
110
|
+
@composite_plan.branches.first.agent,
|
|
111
|
+
:agent
|
|
112
|
+
)
|
|
113
|
+
validate_data_volume!(output, agent)
|
|
114
|
+
run_output_guardrails(output, agent)
|
|
115
|
+
else
|
|
116
|
+
run_workflow_output_guardrails(output)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DefinitionBoundary
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def restart_safe_split_step? = !effective_definition_digest.nil?
|
|
10
|
+
|
|
11
|
+
def ensure_split_step_definition_current!
|
|
12
|
+
return if effective_definition_digest == self.class.definition_digest
|
|
13
|
+
|
|
14
|
+
raise WorkflowError, "the prepared workflow definition has changed"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchBoundary
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def dispatch_store!(...)
|
|
10
|
+
@split_step_dispatch_started = true if @split_step_phase == :preparing
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchClaim
|
|
7
|
+
def claim_prepared_step_dispatch!
|
|
8
|
+
ensure_restart_safe_dispatch_claim!
|
|
9
|
+
ensure_split_step_definition_current!
|
|
10
|
+
transaction_identity = TransactionIdentity.capture(@split_step_adapter)
|
|
11
|
+
claim_dispatch_intent!
|
|
12
|
+
payload, descriptor = build_split_step_dispatch_claim
|
|
13
|
+
dispatch_started = true
|
|
14
|
+
replace_exact_dispatch!(payload)
|
|
15
|
+
accept_split_step_dispatch!(payload, descriptor, transaction_identity)
|
|
16
|
+
descriptor
|
|
17
|
+
rescue PersistencePayloadConflict
|
|
18
|
+
reject_split_step_dispatch!
|
|
19
|
+
raise
|
|
20
|
+
rescue StandardError
|
|
21
|
+
if dispatch_started
|
|
22
|
+
mark_split_step_dispatch_unknown!(payload, descriptor)
|
|
23
|
+
else
|
|
24
|
+
restore_unclaimed_dispatch!
|
|
25
|
+
end
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def ensure_restart_safe_dispatch_claim!
|
|
32
|
+
return if restart_safe_split_step?
|
|
33
|
+
|
|
34
|
+
raise WorkflowError, "restart-safe dispatch claiming requires a definition_digest"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def split_step_dispatch_payload(token)
|
|
38
|
+
document = JSON.parse(@split_step_preparation_payload)
|
|
39
|
+
document["split_step_phase"] = "dispatching"
|
|
40
|
+
document["split_step_dispatch_token"] = token
|
|
41
|
+
JSON.generate(document)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_split_step_dispatch_claim
|
|
45
|
+
token = SecureRandom.uuid.freeze
|
|
46
|
+
payload = split_step_dispatch_payload(token)
|
|
47
|
+
[payload, build_split_step_dispatch(token, payload)]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def replace_exact_dispatch!(payload)
|
|
51
|
+
@split_step_adapter.replace_exact(
|
|
52
|
+
@split_step_persistence_key,
|
|
53
|
+
payload,
|
|
54
|
+
expected_payload: @split_step_preparation_payload,
|
|
55
|
+
ttl: nil
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_split_step_dispatch(token, payload)
|
|
60
|
+
PreparedStepDispatch.new(
|
|
61
|
+
prepared_step: @split_step_prepared_descriptor,
|
|
62
|
+
token:,
|
|
63
|
+
dispatch_digest: CanonicalPayloadDigest.call(payload)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def accept_split_step_dispatch!(payload, descriptor, transaction_identity)
|
|
68
|
+
@split_step_mutex.synchronize do
|
|
69
|
+
unless active_split_step_dispatch_claim?
|
|
70
|
+
raise WorkflowError, "the prepared dispatch claim is no longer active"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@split_step_pre_dispatch_payload = @split_step_preparation_payload
|
|
74
|
+
@split_step_preparation_payload = payload.freeze
|
|
75
|
+
@split_step_dispatch_token = descriptor.token
|
|
76
|
+
@split_step_dispatch_descriptor = descriptor
|
|
77
|
+
@split_step_dispatch_thread = nil
|
|
78
|
+
@split_step_phase = transaction_identity ? :dispatch_claimed_uncommitted : :dispatch_claimed
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def reject_split_step_dispatch!
|
|
83
|
+
@split_step_mutex.synchronize do
|
|
84
|
+
next unless active_split_step_dispatch_claim?
|
|
85
|
+
|
|
86
|
+
@split_step_dispatch_thread = nil
|
|
87
|
+
@split_step_phase = :dispatch_rejected
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def restore_unclaimed_dispatch!
|
|
92
|
+
@split_step_mutex.synchronize do
|
|
93
|
+
next unless active_split_step_dispatch_claim?
|
|
94
|
+
|
|
95
|
+
@split_step_dispatch_thread = nil
|
|
96
|
+
@split_step_phase = :prepared
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def mark_split_step_dispatch_unknown!(payload, descriptor)
|
|
101
|
+
@split_step_mutex.synchronize do
|
|
102
|
+
next unless active_split_step_dispatch_claim?
|
|
103
|
+
|
|
104
|
+
@split_step_attempted_dispatch_payload = payload&.freeze
|
|
105
|
+
@split_step_dispatch_token = descriptor&.token
|
|
106
|
+
@split_step_attempted_dispatch_descriptor = descriptor
|
|
107
|
+
@split_step_dispatch_thread = nil
|
|
108
|
+
@split_step_phase = :dispatch_unknown
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def active_split_step_dispatch_claim?
|
|
113
|
+
@split_step_phase == :claiming_dispatch && @split_step_dispatch_thread.equal?(Thread.current)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchConfirmation
|
|
7
|
+
def confirm_prepared_step_dispatch!
|
|
8
|
+
confirmation_claimed = false
|
|
9
|
+
rolled_back = false
|
|
10
|
+
claim_dispatch_confirmation!
|
|
11
|
+
confirmation_claimed = true
|
|
12
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
13
|
+
return confirm_committed_dispatch! if persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
14
|
+
|
|
15
|
+
rolled_back = recover_rolled_back_dispatch!(payload)
|
|
16
|
+
raise WorkflowError, "the persisted split-step dispatch claim is not committed"
|
|
17
|
+
rescue StandardError
|
|
18
|
+
mark_unconfirmed_dispatch_unknown! if confirmation_claimed && !rolled_back
|
|
19
|
+
raise
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def claim_dispatch_confirmation!
|
|
25
|
+
@split_step_mutex.synchronize do
|
|
26
|
+
unless @split_step_phase == :dispatch_claimed_uncommitted
|
|
27
|
+
raise WorkflowError, "no uncommitted split-step dispatch claim is awaiting confirmation"
|
|
28
|
+
end
|
|
29
|
+
if @split_step_adapter.transaction_open?
|
|
30
|
+
raise WorkflowError, "the split-step dispatch transaction is still open"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@split_step_phase = :confirming_dispatch
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def confirm_committed_dispatch!
|
|
38
|
+
@split_step_mutex.synchronize do
|
|
39
|
+
@split_step_phase = :dispatch_claimed
|
|
40
|
+
clear_split_step_dispatch_transaction!
|
|
41
|
+
end
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def recover_rolled_back_dispatch!(payload)
|
|
46
|
+
return false unless persisted_split_step_payload?(payload, @split_step_pre_dispatch_payload)
|
|
47
|
+
|
|
48
|
+
@split_step_mutex.synchronize do
|
|
49
|
+
return false unless @split_step_phase == :confirming_dispatch
|
|
50
|
+
|
|
51
|
+
@split_step_preparation_payload = @split_step_pre_dispatch_payload
|
|
52
|
+
@split_step_phase = :prepared
|
|
53
|
+
clear_split_step_dispatch_transaction!
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mark_unconfirmed_dispatch_unknown!
|
|
59
|
+
@split_step_mutex.synchronize do
|
|
60
|
+
@split_step_phase = :dispatch_unknown if @split_step_phase == :confirming_dispatch
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear_split_step_dispatch_transaction!
|
|
65
|
+
@split_step_pre_dispatch_payload = nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module DispatchVerification
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def claim_dispatch_intent!
|
|
10
|
+
@split_step_mutex.synchronize do
|
|
11
|
+
raise WorkflowError, "no persisted step is prepared" unless @split_step_phase == :prepared
|
|
12
|
+
|
|
13
|
+
ensure_prepared_split_step_transition_matches!
|
|
14
|
+
@split_step_phase = :claiming_dispatch
|
|
15
|
+
@split_step_dispatch_thread = Thread.current
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify_split_step_dispatch_available!(verification_token)
|
|
20
|
+
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
21
|
+
return if persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
22
|
+
|
|
23
|
+
@split_step_mutex.synchronize do
|
|
24
|
+
if active_split_step_execution_verification?(verification_token)
|
|
25
|
+
@split_step_phase = :dispatch_unknown
|
|
26
|
+
@split_step_execution_previous_phase = nil
|
|
27
|
+
clear_split_step_execution_verification!
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
raise PersistencePayloadConflict.new(key: @split_step_persistence_key)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -5,74 +5,38 @@ module Smith
|
|
|
5
5
|
module SplitStepPersistence
|
|
6
6
|
module Execution
|
|
7
7
|
def execute_prepared_step!
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
verify_split_step_preparation_available!
|
|
13
|
-
activate_split_step_execution!
|
|
14
|
-
execution_started = true
|
|
15
|
-
step = execute_claimed_split_step_transition!
|
|
16
|
-
ensure
|
|
17
|
-
execution_started ? finish_split_step_execution!(step) : restore_unverified_execution!
|
|
18
|
-
end
|
|
19
|
-
step
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def prepared_persisted_step? = @split_step_mutex.synchronize { @split_step_phase == :prepared }
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def claim_split_step_execution_verification!
|
|
27
|
-
@split_step_mutex.synchronize do
|
|
28
|
-
raise WorkflowError, "no persisted step is prepared" unless @split_step_phase == :prepared
|
|
29
|
-
raise WorkflowError, "the prepared transition no longer matches the workflow" unless
|
|
30
|
-
prepared_split_step_transition_matches?
|
|
31
|
-
|
|
32
|
-
@split_step_phase = :verifying_execution
|
|
8
|
+
result = ExecutionAuthorizationIssuance
|
|
9
|
+
.instance_method(:with_prepared_step_execution_authorization)
|
|
10
|
+
.bind_call(self) do |authorization|
|
|
11
|
+
Execution.instance_method(:execute_authorized_prepared_step!).bind_call(self, authorization)
|
|
33
12
|
end
|
|
13
|
+
result.step_snapshot
|
|
34
14
|
end
|
|
35
15
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
transition.equal?(@split_step_transition) &&
|
|
41
|
-
split_step_transition_signature(transition) == @split_step_transition_signature
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def activate_split_step_execution!
|
|
45
|
-
@split_step_mutex.synchronize do
|
|
46
|
-
unless @split_step_phase == :verifying_execution
|
|
47
|
-
raise WorkflowError, "the prepared execution claim is no longer active"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
@split_step_phase = :executing
|
|
51
|
-
@split_step_execution_thread = Thread.current
|
|
52
|
-
@split_step_advance_permit = true
|
|
16
|
+
def execute_authorized_prepared_step!(authorization)
|
|
17
|
+
perform_authorized_prepared_step_execution!(authorization) do |execution_thread|
|
|
18
|
+
step = execute_claimed_split_step_transition!
|
|
19
|
+
[step, consume_split_step_execution_result!(execution_thread)]
|
|
53
20
|
end
|
|
54
21
|
end
|
|
55
22
|
|
|
56
|
-
def
|
|
23
|
+
def prepared_persisted_step?
|
|
57
24
|
@split_step_mutex.synchronize do
|
|
58
|
-
|
|
25
|
+
expected = restart_safe_split_step? ? :dispatch_claimed : :prepared
|
|
26
|
+
[expected, :execution_authorized].include?(@split_step_phase)
|
|
59
27
|
end
|
|
60
28
|
end
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
payload = @split_step_adapter.fetch(@split_step_persistence_key)
|
|
64
|
-
durable = persisted_split_step_payload?(payload, @split_step_preparation_payload)
|
|
65
|
-
live = persisted_split_step_payload?(current_split_step_preparation_payload, @split_step_preparation_payload)
|
|
66
|
-
stable = split_step_transition_signature(@split_step_transition) == @split_step_transition_signature
|
|
67
|
-
return if durable && live && stable
|
|
30
|
+
private
|
|
68
31
|
|
|
69
|
-
|
|
70
|
-
|
|
32
|
+
def restore_unverified_execution!(verification_token)
|
|
33
|
+
@split_step_mutex.synchronize do
|
|
34
|
+
next unless active_split_step_execution_verification?(verification_token)
|
|
71
35
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
36
|
+
@split_step_phase = @split_step_execution_previous_phase
|
|
37
|
+
@split_step_execution_previous_phase = nil
|
|
38
|
+
clear_split_step_execution_verification!
|
|
39
|
+
end
|
|
76
40
|
end
|
|
77
41
|
|
|
78
42
|
def execute_claimed_split_step_transition!
|
|
@@ -82,14 +46,6 @@ module Smith
|
|
|
82
46
|
raise WorkflowError, "prepared execution did not return the claimed transition"
|
|
83
47
|
end
|
|
84
48
|
|
|
85
|
-
def finish_split_step_execution!(step)
|
|
86
|
-
@split_step_mutex.synchronize do
|
|
87
|
-
@split_step_execution_thread = nil
|
|
88
|
-
@split_step_advance_permit = false
|
|
89
|
-
@split_step_phase = step ? :executed : :attempted
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
49
|
def resolve_split_step_advance_transition
|
|
94
50
|
@split_step_mutex.synchronize do
|
|
95
51
|
unless @split_step_phase == :executing && @split_step_execution_thread.equal?(Thread.current)
|