smith-agents 0.4.2 → 0.4.4
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 +81 -3
- data/README.md +45 -2
- data/UPSTREAM_PROPOSAL.md +19 -0
- data/docs/PATTERNS.md +41 -1
- data/lib/smith/agent/lifecycle.rb +30 -10
- 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 +2 -0
- 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 +5 -8
- data/lib/smith/workflow/event_integration.rb +3 -3
- data/lib/smith/workflow/execution.rb +2 -0
- data/lib/smith/workflow/fanout_execution.rb +2 -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 +61 -20
- data/lib/smith/workflow/graph.rb +16 -1
- data/lib/smith/workflow/graph_dsl.rb +4 -0
- data/lib/smith/workflow/guardrail_integration.rb +20 -3
- 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 +3 -15
- data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
- data/lib/smith/workflow/parallel.rb +6 -15
- data/lib/smith/workflow/parallel_execution.rb +2 -0
- data/lib/smith/workflow/persistence.rb +37 -6
- data/lib/smith/workflow/router.rb +15 -4
- data/lib/smith/workflow/run_result.rb +54 -0
- data/lib/smith/workflow/transition.rb +90 -4
- 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 +9 -0
- metadata +21 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
class RuntimeReadinessReport
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
option :workflow_class
|
|
12
|
+
option :topology_report
|
|
13
|
+
option :runtime_diagnostics
|
|
14
|
+
option :metrics
|
|
15
|
+
|
|
16
|
+
def ready?
|
|
17
|
+
errors.empty?
|
|
18
|
+
end
|
|
19
|
+
alias valid? ready?
|
|
20
|
+
|
|
21
|
+
def status
|
|
22
|
+
return :not_ready if errors.any?
|
|
23
|
+
return :warning if warnings.any?
|
|
24
|
+
|
|
25
|
+
:ready
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def topology_status
|
|
29
|
+
topology_report.status
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def diagnostics
|
|
33
|
+
topology_report.diagnostics + runtime_diagnostics
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def errors
|
|
37
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :error }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warnings
|
|
41
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :warning }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def suggestions
|
|
45
|
+
diagnostics.map(&:suggestion).compact.uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
{
|
|
50
|
+
status: status,
|
|
51
|
+
workflow_class: workflow_class,
|
|
52
|
+
topology_status: topology_status,
|
|
53
|
+
ready: ready?,
|
|
54
|
+
diagnostics: diagnostics.map(&:to_h),
|
|
55
|
+
runtime_diagnostics: runtime_diagnostics.map(&:to_h),
|
|
56
|
+
topology_diagnostics: topology_report.diagnostics.map(&:to_h),
|
|
57
|
+
suggestions: suggestions,
|
|
58
|
+
metrics: metrics,
|
|
59
|
+
graph: topology_report.to_h
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -21,6 +21,7 @@ module Smith
|
|
|
21
21
|
def names
|
|
22
22
|
names = [transition.success_transition, transition.failure_transition]
|
|
23
23
|
names.concat(router_names)
|
|
24
|
+
names.concat(deterministic_route_names)
|
|
24
25
|
names.compact.uniq
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -32,6 +33,10 @@ module Smith
|
|
|
32
33
|
transition.router_config.fetch(:fallback)
|
|
33
34
|
]
|
|
34
35
|
end
|
|
36
|
+
|
|
37
|
+
def deterministic_route_names
|
|
38
|
+
transition.deterministic_routes || []
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
end
|
|
37
42
|
end
|
|
@@ -16,6 +16,7 @@ module Smith
|
|
|
16
16
|
transition_target_diagnostic(transition, :success_transition, transition.success_transition),
|
|
17
17
|
transition_target_diagnostic(transition, :failure_transition, transition.failure_transition),
|
|
18
18
|
*router_target_diagnostics(transition),
|
|
19
|
+
*deterministic_route_diagnostics(transition),
|
|
19
20
|
*target_state_mismatch_diagnostics(transition)
|
|
20
21
|
].compact
|
|
21
22
|
end
|
|
@@ -45,6 +46,12 @@ module Smith
|
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
def deterministic_route_diagnostics(transition)
|
|
50
|
+
Array(transition.deterministic_routes).filter_map do |target|
|
|
51
|
+
transition_target_diagnostic(transition, :deterministic_route, target)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
48
55
|
def target_state_mismatch_diagnostics(transition)
|
|
49
56
|
Targets.for(transition).filter_map do |target_name|
|
|
50
57
|
target = graph.transitions[target_name]
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "fanout_contract"
|
|
6
|
+
require_relative "optimization_contract"
|
|
7
|
+
require_relative "orchestration_contract"
|
|
8
|
+
|
|
3
9
|
module Smith
|
|
4
10
|
class Workflow
|
|
5
11
|
class Graph
|
|
6
12
|
class TransitionSnapshot
|
|
13
|
+
extend Dry::Initializer
|
|
14
|
+
|
|
7
15
|
KINDS = [
|
|
8
16
|
%i[deterministic deterministic?],
|
|
9
17
|
%i[router routed?],
|
|
@@ -14,22 +22,64 @@ module Smith
|
|
|
14
22
|
%i[parallel parallel?]
|
|
15
23
|
].freeze
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
option :name
|
|
26
|
+
option :from
|
|
27
|
+
option :to
|
|
28
|
+
option :kind
|
|
29
|
+
option :success_transition, default: proc {}
|
|
30
|
+
option :failure_transition, default: proc {}
|
|
31
|
+
option :routes, default: proc {}
|
|
32
|
+
option :fallback, default: proc {}
|
|
33
|
+
option :deterministic_routes, default: proc {}
|
|
34
|
+
option :fanout_branches, default: proc {}
|
|
35
|
+
option :fanout, default: proc {}
|
|
36
|
+
option :optimization, default: proc {}
|
|
37
|
+
option :orchestration, default: proc {}
|
|
38
|
+
option :retry_policy, default: proc {}
|
|
39
|
+
|
|
40
|
+
def self.from_transition(transition, workflow_class: nil)
|
|
41
|
+
new(**attributes_for(transition, workflow_class: workflow_class))
|
|
42
|
+
end
|
|
19
43
|
|
|
20
|
-
def self.
|
|
21
|
-
|
|
44
|
+
def self.attributes_for(transition, workflow_class:)
|
|
45
|
+
{
|
|
22
46
|
name: transition.name,
|
|
23
47
|
from: transition.from,
|
|
24
48
|
to: transition.to,
|
|
25
|
-
kind: kind_for(transition)
|
|
49
|
+
kind: kind_for(transition)
|
|
50
|
+
}.merge(routing_attributes(transition), contract_attributes(transition, workflow_class: workflow_class))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.routing_attributes(transition)
|
|
54
|
+
{
|
|
26
55
|
success_transition: transition.success_transition,
|
|
27
56
|
failure_transition: transition.failure_transition,
|
|
28
57
|
routes: transition.router_config&.fetch(:routes, nil),
|
|
29
58
|
fallback: transition.router_config&.fetch(:fallback, nil),
|
|
30
|
-
|
|
59
|
+
deterministic_routes: transition.deterministic_routes,
|
|
60
|
+
fanout_branches: transition.fanout_config&.fetch(:branches, nil)
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.contract_attributes(transition, workflow_class:)
|
|
65
|
+
{
|
|
66
|
+
fanout: fanout_for(transition, workflow_class: workflow_class),
|
|
67
|
+
optimization: optimization_for(transition, workflow_class: workflow_class),
|
|
68
|
+
orchestration: orchestration_for(transition, workflow_class: workflow_class),
|
|
31
69
|
retry_policy: retry_policy_for(transition)
|
|
32
|
-
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.fanout_for(transition, workflow_class:)
|
|
74
|
+
FanoutContract.from_transition(transition, workflow_class: workflow_class)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.optimization_for(transition, workflow_class:)
|
|
78
|
+
OptimizationContract.from_transition(transition, workflow_class: workflow_class)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.orchestration_for(transition, workflow_class:)
|
|
82
|
+
OrchestrationContract.from_transition(transition, workflow_class: workflow_class)
|
|
33
83
|
end
|
|
34
84
|
|
|
35
85
|
def self.retry_policy_for(transition)
|
|
@@ -53,19 +103,6 @@ module Smith
|
|
|
53
103
|
:noop
|
|
54
104
|
end
|
|
55
105
|
|
|
56
|
-
def initialize(**attributes)
|
|
57
|
-
@name = attributes.fetch(:name)
|
|
58
|
-
@from = attributes.fetch(:from)
|
|
59
|
-
@to = attributes.fetch(:to)
|
|
60
|
-
@kind = attributes.fetch(:kind)
|
|
61
|
-
@success_transition = attributes[:success_transition]
|
|
62
|
-
@failure_transition = attributes[:failure_transition]
|
|
63
|
-
@routes = attributes[:routes]
|
|
64
|
-
@fallback = attributes[:fallback]
|
|
65
|
-
@fanout_branches = attributes[:fanout_branches]
|
|
66
|
-
@retry_policy = attributes[:retry_policy]
|
|
67
|
-
end
|
|
68
|
-
|
|
69
106
|
def to_h
|
|
70
107
|
{
|
|
71
108
|
name: name,
|
|
@@ -76,7 +113,11 @@ module Smith
|
|
|
76
113
|
failure_transition: failure_transition,
|
|
77
114
|
routes: routes,
|
|
78
115
|
fallback: fallback,
|
|
116
|
+
deterministic_routes: deterministic_routes,
|
|
79
117
|
fanout_branches: fanout_branches,
|
|
118
|
+
fanout: fanout,
|
|
119
|
+
optimization: optimization,
|
|
120
|
+
orchestration: orchestration,
|
|
80
121
|
retry_policy: retry_policy
|
|
81
122
|
}.compact
|
|
82
123
|
end
|
data/lib/smith/workflow/graph.rb
CHANGED
|
@@ -16,8 +16,14 @@ module Smith
|
|
|
16
16
|
Validator.new(self).report
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def runtime_readiness(visited: nil)
|
|
20
|
+
RuntimeReadiness.new(self, visited: visited).report
|
|
21
|
+
end
|
|
22
|
+
|
|
19
23
|
def transition_snapshots
|
|
20
|
-
transitions.values.map
|
|
24
|
+
transitions.values.map do |transition|
|
|
25
|
+
TransitionSnapshot.from_transition(transition, workflow_class: workflow_class)
|
|
26
|
+
end
|
|
21
27
|
end
|
|
22
28
|
end
|
|
23
29
|
end
|
|
@@ -29,8 +35,17 @@ require_relative "graph/state_diagnostics"
|
|
|
29
35
|
require_relative "graph/reachability"
|
|
30
36
|
require_relative "graph/reachability_diagnostics"
|
|
31
37
|
require_relative "graph/metrics"
|
|
38
|
+
require_relative "graph/nested_readiness_diagnostics"
|
|
32
39
|
require_relative "graph/report"
|
|
40
|
+
require_relative "graph/runtime_binding_diagnostic_builder"
|
|
41
|
+
require_relative "graph/runtime_binding_diagnostics"
|
|
42
|
+
require_relative "graph/runtime_readiness_report"
|
|
43
|
+
require_relative "graph/runtime_readiness_metrics"
|
|
33
44
|
require_relative "graph/targets"
|
|
45
|
+
require_relative "graph/fanout_contract"
|
|
46
|
+
require_relative "graph/optimization_contract"
|
|
47
|
+
require_relative "graph/orchestration_contract"
|
|
34
48
|
require_relative "graph/transition_snapshot"
|
|
35
49
|
require_relative "graph/transition_diagnostics"
|
|
36
50
|
require_relative "graph/validator"
|
|
51
|
+
require_relative "graph/runtime_readiness"
|
|
@@ -20,16 +20,33 @@ module Smith
|
|
|
20
20
|
run_agent_output_guardrails(output, agent_class)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def handle_step_failure(transition,
|
|
23
|
+
def handle_step_failure(transition, error)
|
|
24
24
|
failure_name = transition.failure_transition
|
|
25
|
-
|
|
25
|
+
raise error unless failure_name
|
|
26
26
|
|
|
27
27
|
fail_transition = self.class.find_transition(failure_name)
|
|
28
|
-
|
|
28
|
+
raise error unless fail_transition
|
|
29
|
+
validate_transition_origin!(fail_transition)
|
|
30
|
+
|
|
31
|
+
if actionable_failure_transition?(fail_transition)
|
|
32
|
+
@next_transition_name = failure_name
|
|
33
|
+
return
|
|
34
|
+
end
|
|
29
35
|
|
|
30
36
|
@state = fail_transition.to
|
|
31
37
|
end
|
|
32
38
|
|
|
39
|
+
def actionable_failure_transition?(transition)
|
|
40
|
+
transition.agent_name ||
|
|
41
|
+
transition.deterministic? ||
|
|
42
|
+
transition.routed? ||
|
|
43
|
+
transition.fanout? ||
|
|
44
|
+
transition.nested? ||
|
|
45
|
+
transition.optimized? ||
|
|
46
|
+
transition.orchestrated? ||
|
|
47
|
+
transition.success_transition
|
|
48
|
+
end
|
|
49
|
+
|
|
33
50
|
def run_workflow_input_guardrails
|
|
34
51
|
wf_guardrails = self.class.guardrails
|
|
35
52
|
Guardrails::Runner.run_inputs(wf_guardrails, @context) if wf_guardrails
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "usage_entry"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
class Workflow
|
|
5
7
|
module NestedExecution
|
|
@@ -29,10 +31,9 @@ module Smith
|
|
|
29
31
|
# Roll up child totals AND usage_entries BEFORE the failed-step
|
|
30
32
|
# check raises. Previously the rollup only fired on child success
|
|
31
33
|
# — billable agent work inside a failed child was silently
|
|
32
|
-
# dropped from the parent's totals/entries.
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# incorrectly). Roll up first, then re-raise, so the parent's
|
|
34
|
+
# dropped from the parent's totals/entries. Simple aggregate drift
|
|
35
|
+
# checks can miss this when parent rollups and entries undercount
|
|
36
|
+
# the same way. Roll up first, then re-raise, so the parent's
|
|
36
37
|
# terminal state reflects the child's billable work even when the
|
|
37
38
|
# child failed.
|
|
38
39
|
def handle_child_result(child_result)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
OptimizationState = Struct.new(
|
|
6
|
+
:config, :prepared_input, :candidate, :feedback, :last_score, :generator_class, :evaluator_class
|
|
7
|
+
) do
|
|
8
|
+
def initialize(config, prepared_input)
|
|
9
|
+
super(config, prepared_input, nil, nil, nil, nil, nil)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
OrchestrationState = Struct.new(
|
|
6
|
+
:config, :prepared_input, :orchestrator_class, :worker_class, :worker_results
|
|
7
|
+
) do
|
|
8
|
+
def initialize(config, prepared_input)
|
|
9
|
+
super(config, prepared_input, nil, nil, nil)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,25 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
require_relative "orchestration_state"
|
|
6
|
+
require_relative "worker_execution"
|
|
5
7
|
|
|
6
8
|
module Smith
|
|
7
9
|
class Workflow
|
|
8
10
|
module OrchestratorWorker
|
|
9
|
-
OrchestrationState = Struct.new(
|
|
10
|
-
:config, :prepared_input, :orchestrator_class, :worker_class, :worker_results
|
|
11
|
-
) do
|
|
12
|
-
def initialize(config, prepared_input)
|
|
13
|
-
super(config, prepared_input, nil, nil, nil)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
WorkerExecution = Struct.new(:execution_id, :task, :output) do
|
|
18
|
-
def self.run(worker_class, task, schema, budget_runner)
|
|
19
|
-
new(SecureRandom.uuid, task, budget_runner.call(worker_class, task, schema))
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
11
|
private
|
|
24
12
|
|
|
25
13
|
def dispatch_step(transition, prepared_input: nil)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Parallel
|
|
6
|
+
CancellationSignal = Struct.new(:cancelled, :mutex) do
|
|
7
|
+
def initialize
|
|
8
|
+
super(false, Mutex.new)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def cancel!
|
|
12
|
+
mutex.synchronize { self.cancelled = true }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cancelled?
|
|
16
|
+
mutex.synchronize { cancelled }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -2,26 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require "concurrent"
|
|
4
4
|
|
|
5
|
+
require_relative "parallel/cancellation_signal"
|
|
6
|
+
|
|
5
7
|
module Smith
|
|
6
8
|
class Workflow
|
|
7
9
|
class Parallel
|
|
8
|
-
CancellationSignal = Struct.new(:cancelled, :mutex) do
|
|
9
|
-
def initialize
|
|
10
|
-
super(false, Mutex.new)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def cancel!
|
|
14
|
-
mutex.synchronize { self.cancelled = true }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def cancelled?
|
|
18
|
-
mutex.synchronize { cancelled }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
10
|
def self.resolve_branch_count(transition, context)
|
|
23
11
|
count = transition.agent_opts[:count]
|
|
24
|
-
count.respond_to?(:call) ? count.call(context) : (count || 1)
|
|
12
|
+
resolved = count.respond_to?(:call) ? count.call(context) : (count || 1)
|
|
13
|
+
return resolved if resolved.is_a?(Integer) && resolved.positive?
|
|
14
|
+
|
|
15
|
+
raise WorkflowError, "parallel branch count must be a positive integer"
|
|
25
16
|
end
|
|
26
17
|
|
|
27
18
|
def self.execute(branches:)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "usage_entry"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
class Workflow
|
|
5
7
|
module Persistence
|
|
@@ -20,7 +22,7 @@ module Smith
|
|
|
20
22
|
total_tokens: @total_tokens || 0,
|
|
21
23
|
tool_results: @tool_results || [],
|
|
22
24
|
outcome: snapshot_outcome,
|
|
23
|
-
#
|
|
25
|
+
# Durable usage fields. All wrapped in
|
|
24
26
|
# snapshot_value so non-JSON-safe runtime values (e.g.
|
|
25
27
|
# custom Hash details on DeterministicStepFailure) get the
|
|
26
28
|
# same deep-copy treatment as context/session_messages/etc.
|
|
@@ -163,9 +165,9 @@ module Smith
|
|
|
163
165
|
|
|
164
166
|
h = raw.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
|
|
165
167
|
{
|
|
166
|
-
transition: h[:transition]
|
|
167
|
-
from: h[:from]
|
|
168
|
-
to: h[:to]
|
|
168
|
+
transition: normalize_transition_name(h[:transition]),
|
|
169
|
+
from: normalize_state_name(h[:from]),
|
|
170
|
+
to: normalize_state_name(h[:to]),
|
|
169
171
|
error_class: h[:error_class],
|
|
170
172
|
error_family: h[:error_family],
|
|
171
173
|
error_message: h[:error_message],
|
|
@@ -255,7 +257,7 @@ module Smith
|
|
|
255
257
|
end
|
|
256
258
|
|
|
257
259
|
def normalize_symbol_fields!(normalized)
|
|
258
|
-
normalized[:state] = normalized[:state]
|
|
260
|
+
normalized[:state] = normalize_state_name(normalized[:state])
|
|
259
261
|
if normalized[:outcome].is_a?(Hash) && normalized[:outcome].key?(:kind)
|
|
260
262
|
normalized[:outcome][:kind] = normalized[:outcome][:kind]&.to_sym
|
|
261
263
|
elsif normalized[:outcome].is_a?(Hash) && normalized[:outcome].key?("kind")
|
|
@@ -263,7 +265,36 @@ module Smith
|
|
|
263
265
|
end
|
|
264
266
|
return unless normalized.key?(:next_transition_name)
|
|
265
267
|
|
|
266
|
-
normalized[:next_transition_name] = normalized[:next_transition_name]
|
|
268
|
+
normalized[:next_transition_name] = normalize_transition_name(normalized[:next_transition_name])
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def normalize_transition_name(value)
|
|
272
|
+
return if value.nil?
|
|
273
|
+
transition = self.class.find_transition(value)
|
|
274
|
+
return transition.name if transition
|
|
275
|
+
return value unless value.is_a?(String)
|
|
276
|
+
|
|
277
|
+
symbolized = value.to_sym
|
|
278
|
+
transition = self.class.find_transition(symbolized)
|
|
279
|
+
return transition.name if transition
|
|
280
|
+
|
|
281
|
+
symbolized
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def normalize_state_name(value)
|
|
285
|
+
return if value.nil?
|
|
286
|
+
return value if declared_state?(value)
|
|
287
|
+
return value unless value.is_a?(String)
|
|
288
|
+
|
|
289
|
+
symbolized = value.to_sym
|
|
290
|
+
return symbolized if declared_state?(symbolized)
|
|
291
|
+
|
|
292
|
+
symbolized
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def declared_state?(value)
|
|
296
|
+
states = self.class.instance_variable_get(:@states) || []
|
|
297
|
+
states.include?(value)
|
|
267
298
|
end
|
|
268
299
|
|
|
269
300
|
def normalize_nested_hashes!(normalized)
|
|
@@ -4,16 +4,23 @@ module Smith
|
|
|
4
4
|
class Workflow
|
|
5
5
|
class Router
|
|
6
6
|
def self.resolve(classifier_output, config, workflow_class:)
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
output = normalize_output(classifier_output)
|
|
8
|
+
validate!(output, config)
|
|
9
|
+
transition_name = select_transition(output, config)
|
|
9
10
|
validate_transition_exists!(transition_name, workflow_class)
|
|
10
11
|
transition_name
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
def self.normalize_output(output)
|
|
15
|
+
return output unless output.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
output.transform_keys { |key| key.is_a?(String) ? key.to_sym : key }
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
def self.validate!(output, config)
|
|
14
21
|
validate_structure!(output)
|
|
15
22
|
validate_confidence!(output[:confidence])
|
|
16
|
-
validate_route_key!(output[:route]
|
|
23
|
+
validate_route_key!(normalize_route_key(output[:route]), output[:confidence], config)
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
def self.validate_structure!(output)
|
|
@@ -37,12 +44,16 @@ module Smith
|
|
|
37
44
|
|
|
38
45
|
def self.select_transition(output, config)
|
|
39
46
|
if output[:confidence] >= config[:confidence_threshold]
|
|
40
|
-
config[:routes][output[:route]
|
|
47
|
+
config[:routes][normalize_route_key(output[:route])]
|
|
41
48
|
else
|
|
42
49
|
config[:fallback]
|
|
43
50
|
end
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
def self.normalize_route_key(route)
|
|
54
|
+
route.to_s.strip.to_sym
|
|
55
|
+
end
|
|
56
|
+
|
|
46
57
|
def self.validate_transition_exists!(transition_name, workflow_class)
|
|
47
58
|
return if workflow_class.find_transition(transition_name)
|
|
48
59
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
# rubocop:disable Style/RedundantStructKeywordInit
|
|
6
|
+
RunResult = Struct.new(:state, :output, :steps, :total_cost, :total_tokens, :context, :session_messages,
|
|
7
|
+
:tool_results, :outcome, :usage_entries, keyword_init: true) do
|
|
8
|
+
def done?
|
|
9
|
+
state_named?(:done)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def failed?
|
|
13
|
+
state_named?(:failed)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def state_named?(name)
|
|
17
|
+
state == name || state.to_s == name.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def terminal_output
|
|
21
|
+
output
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def outcome_kind
|
|
25
|
+
outcome&.dig(:kind)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def outcome_payload
|
|
29
|
+
outcome&.dig(:payload)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def last_error
|
|
33
|
+
steps.reverse.map { |step| step[:error] }.compact.first
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def failed_transition
|
|
37
|
+
failure_detail&.fetch(:transition)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def failure_detail
|
|
41
|
+
failed_step = steps.reverse.find { |step| step[:error] }
|
|
42
|
+
return nil unless failed_step
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
transition: failed_step[:transition],
|
|
46
|
+
from: failed_step[:from],
|
|
47
|
+
to: failed_step[:to],
|
|
48
|
+
error: failed_step[:error]
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
# rubocop:enable Style/RedundantStructKeywordInit
|
|
53
|
+
end
|
|
54
|
+
end
|