smith-agents 0.4.1 → 0.4.3
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 +91 -3
- data/README.md +69 -3
- data/UPSTREAM_PROPOSAL.md +19 -0
- data/docs/PATTERNS.md +85 -6
- data/lib/smith/agent/lifecycle.rb +11 -7
- data/lib/smith/agent/registry/introspection.rb +27 -0
- data/lib/smith/agent/registry.rb +22 -17
- data/lib/smith/agent/registry_binding.rb +43 -0
- data/lib/smith/agent.rb +3 -4
- data/lib/smith/doctor/checks/models_registry.rb +25 -14
- data/lib/smith/doctor/checks/persistence_capabilities.rb +37 -29
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/agent_result.rb +26 -0
- data/lib/smith/workflow/branch_env.rb +24 -0
- data/lib/smith/workflow/budget_integration.rb +27 -5
- data/lib/smith/workflow/deterministic_execution.rb +1 -1
- data/lib/smith/workflow/deterministic_step.rb +32 -4
- data/lib/smith/workflow/durability.rb +38 -12
- data/lib/smith/workflow/evaluator_optimizer.rb +18 -10
- data/lib/smith/workflow/event_integration.rb +3 -3
- data/lib/smith/workflow/execution.rb +8 -1
- data/lib/smith/workflow/fanout_execution.rb +121 -0
- data/lib/smith/workflow/graph/contract_helpers.rb +65 -0
- data/lib/smith/workflow/graph/fanout_contract.rb +140 -0
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +98 -0
- data/lib/smith/workflow/graph/optimization_contract.rb +96 -0
- data/lib/smith/workflow/graph/orchestration_contract.rb +83 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb +124 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +114 -0
- data/lib/smith/workflow/graph/runtime_readiness.rb +63 -0
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +87 -0
- data/lib/smith/workflow/graph/runtime_readiness_report.rb +65 -0
- data/lib/smith/workflow/graph/targets.rb +5 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +7 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +80 -18
- data/lib/smith/workflow/graph.rb +16 -1
- data/lib/smith/workflow/graph_dsl.rb +4 -0
- data/lib/smith/workflow/guardrail_integration.rb +45 -10
- data/lib/smith/workflow/nested_execution.rb +5 -4
- data/lib/smith/workflow/optimization_state.rb +13 -0
- data/lib/smith/workflow/orchestration_state.rb +13 -0
- data/lib/smith/workflow/orchestrator_worker.rb +5 -16
- data/lib/smith/workflow/parallel/cancellation.rb +9 -0
- data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
- data/lib/smith/workflow/parallel.rb +12 -16
- data/lib/smith/workflow/parallel_execution.rb +5 -1
- data/lib/smith/workflow/persistence.rb +37 -6
- data/lib/smith/workflow/retry_execution.rb +52 -0
- data/lib/smith/workflow/router.rb +15 -4
- data/lib/smith/workflow/run_result.rb +54 -0
- data/lib/smith/workflow/transition.rb +260 -24
- data/lib/smith/workflow/usage_entry.rb +30 -0
- data/lib/smith/workflow/worker_execution.rb +13 -0
- data/lib/smith/workflow.rb +40 -136
- data/lib/smith.rb +12 -0
- metadata +24 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "branch_env"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module FanoutExecution
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def run_guarded_fanout_step(transition)
|
|
11
|
+
branches = transition.fanout_config.fetch(:branches)
|
|
12
|
+
branch_agent_classes = fanout_agent_classes(transition, branches)
|
|
13
|
+
run_workflow_input_guardrails
|
|
14
|
+
run_fanout_agent_input_guardrails(branch_agent_classes)
|
|
15
|
+
prepared_input = build_session&.prepare!
|
|
16
|
+
output = execute_fanout_step(
|
|
17
|
+
transition,
|
|
18
|
+
branches: branches,
|
|
19
|
+
branch_agent_classes: branch_agent_classes,
|
|
20
|
+
prepared_input: prepared_input
|
|
21
|
+
)
|
|
22
|
+
run_workflow_output_guardrails(output)
|
|
23
|
+
output
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute_fanout_step(transition, branches: nil, branch_agent_classes: nil, prepared_input: nil)
|
|
27
|
+
branches ||= transition.fanout_config.fetch(:branches)
|
|
28
|
+
branch_agent_classes ||= fanout_agent_classes(transition, branches)
|
|
29
|
+
env = BranchEnv.new(
|
|
30
|
+
prepared_input: prepared_input,
|
|
31
|
+
guardrail_sources: nil,
|
|
32
|
+
scoped_store: propagate_scoped_artifacts,
|
|
33
|
+
branch_estimates: fanout_branch_estimates(branches, branch_agent_classes),
|
|
34
|
+
deadline: wall_clock_deadline
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
branch_calls = branches.map do |branch_key, agent_name|
|
|
38
|
+
proc do |signal|
|
|
39
|
+
run_fanout_branch(branch_key, agent_name, branch_agent_classes.fetch(branch_key), env, signal)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Parallel.execute(branches: branch_calls)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def run_fanout_branch(branch_key, agent_name, agent_class, env, signal)
|
|
47
|
+
setup_fanout_branch_context(env, @ledger, agent_class)
|
|
48
|
+
|
|
49
|
+
with_agent_context(agent_class) do
|
|
50
|
+
branch_ledger = effective_call_ledger
|
|
51
|
+
reserved = reserve_fanout_branch_call(branch_ledger, env.branch_estimates[branch_key], agent_class)
|
|
52
|
+
begin
|
|
53
|
+
result = guarded_fanout_branch_call(agent_class, env, signal)
|
|
54
|
+
finalize_named_branch(branch_key, agent_name, result, branch_ledger, reserved).tap { reserved = nil }
|
|
55
|
+
ensure
|
|
56
|
+
settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
ensure
|
|
60
|
+
teardown_branch_context(env)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def guarded_fanout_branch_call(agent_class, env, signal)
|
|
64
|
+
check_cancellation!(signal)
|
|
65
|
+
check_deadline!
|
|
66
|
+
result = agent_class.model_configured? ? invoke_agent(agent_class, env.prepared_input) : nil
|
|
67
|
+
output = result.is_a?(AgentResult) ? result.content : result
|
|
68
|
+
validate_data_volume!(output, agent_class)
|
|
69
|
+
run_agent_output_guardrails(output, agent_class)
|
|
70
|
+
check_cancellation!(signal)
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def setup_fanout_branch_context(env, ledger, agent_class)
|
|
75
|
+
setup_branch_context(env, ledger)
|
|
76
|
+
apply_tool_guardrails(agent_class)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reserve_fanout_branch_call(branch_ledger, branch_estimates, agent_class)
|
|
80
|
+
return reserve_branch_budget(branch_ledger, branch_estimates: branch_estimates) if @ledger
|
|
81
|
+
|
|
82
|
+
reserve_serial_budget(branch_ledger, agent_budget: agent_class&.budget) if branch_ledger
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def fanout_agent_classes(transition, branches)
|
|
86
|
+
branches.to_h do |branch_key, agent_name|
|
|
87
|
+
[branch_key, resolve_fanout_agent_class(transition, agent_name)]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_fanout_agent_input_guardrails(branch_agent_classes)
|
|
92
|
+
branch_agent_classes.each_value do |agent_class|
|
|
93
|
+
run_agent_input_guardrails(agent_class)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def fanout_branch_estimates(branches, branch_agent_classes)
|
|
98
|
+
return {} unless @ledger
|
|
99
|
+
|
|
100
|
+
branch_count = branches.length
|
|
101
|
+
branches.each_with_object({}) do |(branch_key, _agent_name), map|
|
|
102
|
+
agent_class = branch_agent_classes.fetch(branch_key)
|
|
103
|
+
map[branch_key] = compute_branch_estimates(
|
|
104
|
+
@ledger,
|
|
105
|
+
branch_count: branch_count,
|
|
106
|
+
agent_budget: agent_class&.budget
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def resolve_fanout_agent_class(transition, agent_name)
|
|
112
|
+
Agent::Registry.fetch!(
|
|
113
|
+
agent_name,
|
|
114
|
+
workflow_class: self.class,
|
|
115
|
+
transition_name: transition&.name,
|
|
116
|
+
role: :fanout_agent
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
module ContractHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def label_for(value)
|
|
10
|
+
label = if value.respond_to?(:name) && value.name && !value.name.empty?
|
|
11
|
+
value.name
|
|
12
|
+
elsif value.respond_to?(:inspect)
|
|
13
|
+
value.inspect
|
|
14
|
+
else
|
|
15
|
+
value.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
immutable_value(label)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def idempotency_mode
|
|
22
|
+
return :unknown unless workflow_class.respond_to?(:idempotency_mode)
|
|
23
|
+
|
|
24
|
+
workflow_class.idempotency_mode
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def in_flight_resume
|
|
28
|
+
return :blocked_by_step_in_progress if idempotency_mode == :strict
|
|
29
|
+
return :unknown unless %i[strict lax].include?(idempotency_mode)
|
|
30
|
+
|
|
31
|
+
:reruns_transition
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def immutable_value(value)
|
|
35
|
+
return value if immediate_value?(value)
|
|
36
|
+
|
|
37
|
+
duplicate = value.dup
|
|
38
|
+
return value if duplicate.equal?(value)
|
|
39
|
+
|
|
40
|
+
duplicate.freeze
|
|
41
|
+
rescue TypeError
|
|
42
|
+
value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def immediate_value?(value)
|
|
46
|
+
value.nil? || value.is_a?(Symbol) || value.is_a?(Numeric) ||
|
|
47
|
+
value == true || value == false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def deep_freeze(value)
|
|
51
|
+
case value
|
|
52
|
+
when Hash
|
|
53
|
+
value.each_value { |nested| deep_freeze(nested) }
|
|
54
|
+
value.freeze
|
|
55
|
+
when Array
|
|
56
|
+
value.each { |nested| deep_freeze(nested) }
|
|
57
|
+
value.freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class FanoutContract
|
|
7
|
+
attr_reader :transition, :workflow_class
|
|
8
|
+
|
|
9
|
+
def self.from_transition(transition, workflow_class: nil)
|
|
10
|
+
branches = transition.fanout_config&.fetch(:branches, nil)
|
|
11
|
+
return unless branches
|
|
12
|
+
|
|
13
|
+
new(transition, workflow_class: workflow_class).to_h
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(transition, workflow_class: nil)
|
|
17
|
+
@transition = transition
|
|
18
|
+
@workflow_class = workflow_class
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
deep_freeze(
|
|
23
|
+
branch_count: branches.length,
|
|
24
|
+
join_state: immutable_value(transition.to),
|
|
25
|
+
output_shape: :named_branch_results,
|
|
26
|
+
branch_order: :declaration_order,
|
|
27
|
+
join: join_contract,
|
|
28
|
+
output_contract: output_contract,
|
|
29
|
+
resume_contract: resume_contract,
|
|
30
|
+
branches: branch_summaries,
|
|
31
|
+
branch_contracts: branch_contracts
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def branches
|
|
38
|
+
transition.fanout_config.fetch(:branches)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def join_contract
|
|
42
|
+
{
|
|
43
|
+
state: immutable_value(transition.to),
|
|
44
|
+
transition: immutable_value(transition.name)
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def output_contract
|
|
49
|
+
{
|
|
50
|
+
collection: :array,
|
|
51
|
+
item_shape: :named_branch_result,
|
|
52
|
+
ordering: :branch_declaration_order,
|
|
53
|
+
branch_key_field: :branch,
|
|
54
|
+
agent_field: :agent,
|
|
55
|
+
output_field: :output,
|
|
56
|
+
failure: :discard_all_branch_results_on_failure
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def resume_contract
|
|
61
|
+
{
|
|
62
|
+
granularity: :transition,
|
|
63
|
+
branch_checkpointing: false,
|
|
64
|
+
idempotency_mode: idempotency_mode,
|
|
65
|
+
in_flight_resume: in_flight_resume
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def branch_summaries
|
|
70
|
+
branches.map do |branch, agent|
|
|
71
|
+
{ branch: immutable_value(branch), agent: immutable_value(agent) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def branch_contracts
|
|
76
|
+
branches.map do |branch, agent|
|
|
77
|
+
branch_value = immutable_value(branch)
|
|
78
|
+
agent_value = immutable_value(agent)
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
branch: branch_value,
|
|
82
|
+
agent: agent_value,
|
|
83
|
+
result_branch_value: branch_value,
|
|
84
|
+
result_shape: {
|
|
85
|
+
branch: branch_value,
|
|
86
|
+
agent: agent_value,
|
|
87
|
+
output: :agent_output
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def immutable_value(value)
|
|
94
|
+
return value if immediate_value?(value)
|
|
95
|
+
|
|
96
|
+
duplicate = value.dup
|
|
97
|
+
return value if duplicate.equal?(value)
|
|
98
|
+
|
|
99
|
+
duplicate.freeze
|
|
100
|
+
rescue TypeError
|
|
101
|
+
# Some host-owned topology values intentionally refuse duplication.
|
|
102
|
+
# Leave them untouched; the graph contract containers are still
|
|
103
|
+
# frozen, and inspection must never mutate workflow-owned values.
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def immediate_value?(value)
|
|
108
|
+
value.nil? || value.is_a?(Symbol) || value.is_a?(Numeric) ||
|
|
109
|
+
value == true || value == false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def idempotency_mode
|
|
113
|
+
return :unknown unless workflow_class&.respond_to?(:idempotency_mode)
|
|
114
|
+
|
|
115
|
+
workflow_class.idempotency_mode
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def in_flight_resume
|
|
119
|
+
return :blocked_by_step_in_progress if idempotency_mode == :strict
|
|
120
|
+
return :unknown unless %i[strict lax].include?(idempotency_mode)
|
|
121
|
+
|
|
122
|
+
:reruns_transition
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def deep_freeze(value)
|
|
126
|
+
case value
|
|
127
|
+
when Hash
|
|
128
|
+
value.each_value { |nested| deep_freeze(nested) }
|
|
129
|
+
value.freeze
|
|
130
|
+
when Array
|
|
131
|
+
value.each { |nested| deep_freeze(nested) }
|
|
132
|
+
value.freeze
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
value
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
class NestedReadinessDiagnostics
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
param :graph
|
|
12
|
+
option :visited, default: proc {}
|
|
13
|
+
|
|
14
|
+
def to_a
|
|
15
|
+
cycle_diagnostics + report_entries.flat_map { |entry| diagnostics_for(entry) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def child_reports
|
|
19
|
+
report_entries.map { |entry| entry.fetch(:report) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def diagnostics_for(entry)
|
|
25
|
+
entry.fetch(:report).diagnostics.map do |diagnostic|
|
|
26
|
+
nested_diagnostic(entry.fetch(:transition), entry.fetch(:workflow_class), diagnostic)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cycle_diagnostics
|
|
31
|
+
nested_workflow_transitions.filter_map do |transition|
|
|
32
|
+
workflow_class = transition.workflow_class
|
|
33
|
+
cycle_diagnostic(transition, workflow_class) if visited_workflows.include?(workflow_class)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def report_entries
|
|
38
|
+
@report_entries ||= nested_workflow_transitions.filter_map do |transition|
|
|
39
|
+
workflow_class = transition.workflow_class
|
|
40
|
+
next if visited_workflows.include?(workflow_class)
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
transition: transition,
|
|
44
|
+
workflow_class: workflow_class,
|
|
45
|
+
report: workflow_class.graph.runtime_readiness(visited: visited_workflows)
|
|
46
|
+
}.freeze
|
|
47
|
+
end.freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def nested_workflow_transitions
|
|
51
|
+
@nested_workflow_transitions ||= graph.transitions.values.select(&:nested?)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cycle_diagnostic(transition, workflow_class)
|
|
55
|
+
label = workflow_label(workflow_class)
|
|
56
|
+
|
|
57
|
+
Diagnostic.new(
|
|
58
|
+
severity: :error,
|
|
59
|
+
code: :nested_workflow_cycle,
|
|
60
|
+
transition: transition.name,
|
|
61
|
+
target: label,
|
|
62
|
+
message: "Transition #{ref(transition.name)} nests #{label}, " \
|
|
63
|
+
"which is already in the readiness inspection stack.",
|
|
64
|
+
suggestion: "Break the nested workflow cycle before runtime execution."
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def nested_diagnostic(transition, workflow_class, diagnostic)
|
|
69
|
+
label = workflow_label(workflow_class)
|
|
70
|
+
|
|
71
|
+
Diagnostic.new(
|
|
72
|
+
severity: diagnostic.severity,
|
|
73
|
+
code: :"nested_#{diagnostic.code}",
|
|
74
|
+
transition: transition.name,
|
|
75
|
+
target: label,
|
|
76
|
+
message: "Nested workflow #{label}: #{diagnostic.message}",
|
|
77
|
+
suggestion: diagnostic.suggestion
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visited_workflows
|
|
82
|
+
@visited_workflows ||= Set.new(Array(visited)).add(graph.workflow_class)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def ref(value)
|
|
86
|
+
Reference.format(value)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def workflow_label(workflow_class)
|
|
90
|
+
name = workflow_class.name
|
|
91
|
+
return name if name && !name.empty?
|
|
92
|
+
|
|
93
|
+
workflow_class.inspect
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "contract_helpers"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
class Graph
|
|
10
|
+
class OptimizationContract
|
|
11
|
+
extend Dry::Initializer
|
|
12
|
+
include ContractHelpers
|
|
13
|
+
|
|
14
|
+
param :transition
|
|
15
|
+
option :workflow_class, default: proc {}
|
|
16
|
+
|
|
17
|
+
def self.from_transition(transition, workflow_class: nil)
|
|
18
|
+
new(transition, workflow_class: workflow_class).to_h if transition.optimization_config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
deep_freeze(
|
|
23
|
+
transition_contract.merge(
|
|
24
|
+
exit_modes: exit_modes,
|
|
25
|
+
output_contract: output_contract,
|
|
26
|
+
resume_contract: resume_contract
|
|
27
|
+
).compact
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def config
|
|
34
|
+
transition.optimization_config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def transition_contract
|
|
38
|
+
{
|
|
39
|
+
generator: immutable_value(config.fetch(:generator)),
|
|
40
|
+
evaluator: immutable_value(config.fetch(:evaluator)),
|
|
41
|
+
max_rounds: config.fetch(:max_rounds)
|
|
42
|
+
}.merge(evaluation_contract)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def evaluation_contract
|
|
46
|
+
{
|
|
47
|
+
evaluator_schema: label_for(config.fetch(:evaluator_schema)),
|
|
48
|
+
evaluator_context: config[:evaluator_context],
|
|
49
|
+
improvement_threshold: config[:improvement_threshold],
|
|
50
|
+
before_eval: callable_label(config[:before_eval])
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def exit_modes
|
|
55
|
+
{
|
|
56
|
+
exhaustion: exit_mode_label(config.fetch(:on_exhaustion)),
|
|
57
|
+
converged: exit_mode_label(config.fetch(:on_converged)),
|
|
58
|
+
threshold: exit_mode_label(config.fetch(:on_threshold))
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def output_contract
|
|
63
|
+
{
|
|
64
|
+
success: :accepted_or_configured_exit_candidate,
|
|
65
|
+
failure: :workflow_error_on_malformed_or_unaccepted_evaluation,
|
|
66
|
+
evaluator_output: {
|
|
67
|
+
required: { accept: :boolean },
|
|
68
|
+
rejection_requires: %i[feedback],
|
|
69
|
+
threshold_requires: %i[score],
|
|
70
|
+
optional: %i[score converged]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def resume_contract
|
|
76
|
+
{
|
|
77
|
+
granularity: :transition,
|
|
78
|
+
round_checkpointing: false,
|
|
79
|
+
idempotency_mode: idempotency_mode,
|
|
80
|
+
in_flight_resume: in_flight_resume
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def exit_mode_label(value)
|
|
85
|
+
return value if value.is_a?(Symbol)
|
|
86
|
+
|
|
87
|
+
callable_label(value)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def callable_label(value)
|
|
91
|
+
value.respond_to?(:call) ? :callable : nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "contract_helpers"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
class Graph
|
|
10
|
+
class OrchestrationContract
|
|
11
|
+
extend Dry::Initializer
|
|
12
|
+
include ContractHelpers
|
|
13
|
+
|
|
14
|
+
param :transition
|
|
15
|
+
option :workflow_class, default: proc {}
|
|
16
|
+
|
|
17
|
+
def self.from_transition(transition, workflow_class: nil)
|
|
18
|
+
new(transition, workflow_class: workflow_class).to_h if transition.orchestrator_config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
deep_freeze(
|
|
23
|
+
transition_contract.merge(
|
|
24
|
+
decision_contract: decision_contract,
|
|
25
|
+
output_contract: output_contract,
|
|
26
|
+
resume_contract: resume_contract
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def config
|
|
34
|
+
transition.orchestrator_config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def transition_contract
|
|
38
|
+
{
|
|
39
|
+
orchestrator: immutable_value(config.fetch(:orchestrator)),
|
|
40
|
+
worker: immutable_value(config.fetch(:worker)),
|
|
41
|
+
max_workers: config.fetch(:max_workers),
|
|
42
|
+
max_delegation_rounds: config.fetch(:max_delegation_rounds),
|
|
43
|
+
worker_dispatch: :serial
|
|
44
|
+
}.merge(schema_contract)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def schema_contract
|
|
48
|
+
{
|
|
49
|
+
task_schema: label_for(config.fetch(:task_schema)),
|
|
50
|
+
worker_output_schema: label_for(config.fetch(:worker_output_schema)),
|
|
51
|
+
final_output_schema: label_for(config.fetch(:final_output_schema))
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def decision_contract
|
|
56
|
+
{
|
|
57
|
+
shape: :hash,
|
|
58
|
+
exactly_one_of: %i[tasks final stop],
|
|
59
|
+
tasks_limit: config.fetch(:max_workers)
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def output_contract
|
|
64
|
+
{
|
|
65
|
+
success: :final_output_schema,
|
|
66
|
+
worker_result_shape: :execution_id_task_output,
|
|
67
|
+
failure: :workflow_error_on_stop_exhaustion_or_schema_violation
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def resume_contract
|
|
72
|
+
{
|
|
73
|
+
granularity: :transition,
|
|
74
|
+
round_checkpointing: false,
|
|
75
|
+
worker_checkpointing: false,
|
|
76
|
+
idempotency_mode: idempotency_mode,
|
|
77
|
+
in_flight_resume: in_flight_resume
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|