smith-agents 0.4.0 → 0.4.1
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 +16 -0
- data/README.md +15 -0
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/graph/diagnostic.rb +33 -0
- data/lib/smith/workflow/graph/metrics.rb +33 -0
- data/lib/smith/workflow/graph/reachability.rb +59 -0
- data/lib/smith/workflow/graph/reachability_diagnostics.rb +47 -0
- data/lib/smith/workflow/graph/reference.rb +15 -0
- data/lib/smith/workflow/graph/report.rb +50 -0
- data/lib/smith/workflow/graph/state_diagnostics.rb +65 -0
- data/lib/smith/workflow/graph/targets.rb +38 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +78 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +65 -0
- data/lib/smith/workflow/graph/validator.rb +50 -0
- data/lib/smith/workflow/graph.rb +36 -0
- data/lib/smith/workflow/graph_dsl.rb +18 -0
- data/lib/smith.rb +3 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fce22e9b87caf5c01417a5e7d8fe7a6f4ee179abebc61644074400452a078791
|
|
4
|
+
data.tar.gz: e7d2ae59746ff545f5d45215bcec20dea95ee82a37d7fa765c4ded95484d4029
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d8515c459487f57c20301581e6f1d5cc786f8bfe7089b4e628548145ac56e0c9c93e9560ac37f1fa7818f95a71a1c8753cb275267b341fb39ca4bf63d5e5f3e
|
|
7
|
+
data.tar.gz: b3e05ca16fc7fec62e8172d9d6ccb84b0bc3f7546af22b9f47c8a36ce4b7e6e5bfb35b0fb47eb51c0b86cf4ab4fbe47e651406c1666fcc63d4b2c16b0ccb0bf9
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
8
8
|
|
|
9
9
|
No unreleased changes.
|
|
10
10
|
|
|
11
|
+
## [0.4.1] - 2026-06-28
|
|
12
|
+
|
|
13
|
+
Patch release for static workflow graph inspection. This is additive and diagnostic-only: Smith exposes declared workflow topology for hosts to render, lint, or cache without executing agents, advancing state, owning progress projection, or changing durability/recovery boundaries.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `Smith::Workflow.graph` — returns a read-only inspection object for a workflow class.
|
|
18
|
+
- `Smith::Workflow.validate_graph` — returns a structured report with validity status, diagnostics, suggestions, transition snapshots, and graph metrics.
|
|
19
|
+
- Pre-runtime graph diagnostics for missing initial states, undefined transition states, unresolved `on_success` / `on_failure` targets, unresolved router route/fallback targets, target-state mismatch warnings, and unreachable-transition warnings.
|
|
20
|
+
- Transition snapshots that preserve declared names exactly and expose `name`, `from`, `to`, `kind`, success/failure targets, router routes, and router fallback.
|
|
21
|
+
|
|
22
|
+
### Test coverage
|
|
23
|
+
|
|
24
|
+
- Default suite: 862 examples, 0 failures.
|
|
25
|
+
- Touched Ruby files: 17 files inspected by RuboCop, 0 offenses.
|
|
26
|
+
|
|
11
27
|
## [0.4.0] - 2026-06-24
|
|
12
28
|
|
|
13
29
|
Two more host-ergonomic primitives that close the deferred-from-0.3.0 backlog: `Workflow.stuck_for?` for liveness probing and `Context.persist :auto` for write-tracked context persistence. Both are purely additive.
|
data/README.md
CHANGED
|
@@ -94,6 +94,21 @@ end
|
|
|
94
94
|
|
|
95
95
|
The full pattern guide with working examples for each lives in [`docs/PATTERNS.md`](docs/PATTERNS.md).
|
|
96
96
|
|
|
97
|
+
## Workflow Graph Inspection
|
|
98
|
+
|
|
99
|
+
Smith can inspect a workflow's declared graph without running agents or advancing state. This is useful for host apps that want to render, lint, or cache a workflow shape before execution.
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
report = ReplyWorkflow.validate_graph
|
|
103
|
+
|
|
104
|
+
report.valid? # => true
|
|
105
|
+
report.transitions # => read-only transition snapshots
|
|
106
|
+
report.diagnostics # => errors and warnings for missing states or routes
|
|
107
|
+
report.metrics # => state, transition, reachability, and terminal-state counts
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Graph inspection is static and diagnostic-only. Runtime execution, persistence, progress projection, retries, and recovery remain host-owned concerns.
|
|
111
|
+
|
|
97
112
|
## Configuration
|
|
98
113
|
|
|
99
114
|
```ruby
|
data/lib/smith/version.rb
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Diagnostic
|
|
7
|
+
attr_reader :severity, :code, :message, :state, :transition, :target, :suggestion
|
|
8
|
+
|
|
9
|
+
def initialize(**attributes)
|
|
10
|
+
@severity = attributes.fetch(:severity)
|
|
11
|
+
@code = attributes.fetch(:code)
|
|
12
|
+
@message = attributes.fetch(:message)
|
|
13
|
+
@state = attributes[:state]
|
|
14
|
+
@transition = attributes[:transition]
|
|
15
|
+
@target = attributes[:target]
|
|
16
|
+
@suggestion = attributes[:suggestion]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
severity: severity,
|
|
22
|
+
code: code,
|
|
23
|
+
message: message,
|
|
24
|
+
state: state,
|
|
25
|
+
transition: transition,
|
|
26
|
+
target: target,
|
|
27
|
+
suggestion: suggestion
|
|
28
|
+
}.compact
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Metrics
|
|
7
|
+
attr_reader :graph, :reachable_transition_names
|
|
8
|
+
|
|
9
|
+
def initialize(graph, reachable_transition_names)
|
|
10
|
+
@graph = graph
|
|
11
|
+
@reachable_transition_names = reachable_transition_names
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
{
|
|
16
|
+
states_count: graph.states.length,
|
|
17
|
+
transitions_count: graph.transitions.length,
|
|
18
|
+
reachable_transitions_count: reachable_transition_names.length,
|
|
19
|
+
terminal_states: terminal_states
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def terminal_states
|
|
26
|
+
graph.states.select do |state|
|
|
27
|
+
graph.transitions.values.none? { |transition| transition.from == state }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Reachability
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def transition_names
|
|
14
|
+
return [] unless graph.initial_state
|
|
15
|
+
|
|
16
|
+
reset_walk
|
|
17
|
+
drain_queue
|
|
18
|
+
@seen_transitions.keys
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def reset_walk
|
|
24
|
+
@seen_states = { graph.initial_state => true }
|
|
25
|
+
@seen_transitions = {}
|
|
26
|
+
@queue = [graph.initial_state]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def drain_queue
|
|
30
|
+
transitions_from(@queue.shift).each { |transition| visit_transition(transition) } until @queue.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_transition(transition)
|
|
34
|
+
return if @seen_transitions.key?(transition.name)
|
|
35
|
+
|
|
36
|
+
@seen_transitions[transition.name] = true
|
|
37
|
+
enqueue_state(transition.to)
|
|
38
|
+
Targets.for(transition).each { |target_name| visit_named_transition(target_name) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit_named_transition(target_name)
|
|
42
|
+
target = graph.transitions[target_name]
|
|
43
|
+
visit_transition(target) if target
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enqueue_state(state)
|
|
47
|
+
return if state.nil? || @seen_states.key?(state)
|
|
48
|
+
|
|
49
|
+
@seen_states[state] = true
|
|
50
|
+
@queue << state
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def transitions_from(state)
|
|
54
|
+
graph.transitions.values.select { |transition| transition.from == state }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class ReachabilityDiagnostics
|
|
7
|
+
attr_reader :graph, :reachable_transition_names
|
|
8
|
+
|
|
9
|
+
def initialize(graph, reachable_transition_names)
|
|
10
|
+
@graph = graph
|
|
11
|
+
@reachable_transition_names = reachable_transition_names
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_a
|
|
15
|
+
(graph.transitions.keys - reachable_transition_names).filter_map do |transition_name|
|
|
16
|
+
next if auto_fail_transition?(transition_name)
|
|
17
|
+
|
|
18
|
+
diagnostic_for(transition_name)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def auto_fail_transition?(transition_name)
|
|
25
|
+
transition_name == :fail && graph.transitions[transition_name]&.from.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def diagnostic_for(transition_name)
|
|
29
|
+
transition = graph.transitions.fetch(transition_name)
|
|
30
|
+
Diagnostic.new(
|
|
31
|
+
severity: :warning,
|
|
32
|
+
code: :unreachable_transition,
|
|
33
|
+
transition: transition_name,
|
|
34
|
+
state: transition.from,
|
|
35
|
+
message: "Transition #{ref(transition_name)} is not reachable from " \
|
|
36
|
+
"initial_state #{ref(graph.initial_state)}.",
|
|
37
|
+
suggestion: "Connect transition #{ref(transition_name)} from a reachable state or remove it."
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ref(value)
|
|
42
|
+
Reference.format(value)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Report
|
|
7
|
+
attr_reader :status, :workflow_class, :initial_state, :states, :transitions, :diagnostics, :metrics
|
|
8
|
+
|
|
9
|
+
def initialize(**attributes)
|
|
10
|
+
@status = attributes.fetch(:status)
|
|
11
|
+
@workflow_class = attributes.fetch(:workflow_class)
|
|
12
|
+
@initial_state = attributes.fetch(:initial_state)
|
|
13
|
+
@states = attributes.fetch(:states)
|
|
14
|
+
@transitions = attributes.fetch(:transitions)
|
|
15
|
+
@diagnostics = attributes.fetch(:diagnostics)
|
|
16
|
+
@metrics = attributes.fetch(:metrics)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def valid?
|
|
20
|
+
errors.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def errors
|
|
24
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :error }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def warnings
|
|
28
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :warning }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def suggestions
|
|
32
|
+
diagnostics.map(&:suggestion).compact.uniq
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
status: status,
|
|
38
|
+
workflow_class: workflow_class,
|
|
39
|
+
initial_state: initial_state,
|
|
40
|
+
states: states,
|
|
41
|
+
transitions: transitions.map(&:to_h),
|
|
42
|
+
diagnostics: diagnostics.map(&:to_h),
|
|
43
|
+
suggestions: suggestions,
|
|
44
|
+
metrics: metrics
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class StateDiagnostics
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_a
|
|
14
|
+
[
|
|
15
|
+
*initial_state_diagnostics,
|
|
16
|
+
*state_reference_diagnostics
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def initial_state_diagnostics
|
|
23
|
+
return [] if graph.initial_state && graph.states.include?(graph.initial_state)
|
|
24
|
+
|
|
25
|
+
[
|
|
26
|
+
Diagnostic.new(
|
|
27
|
+
severity: :error,
|
|
28
|
+
code: :missing_initial_state,
|
|
29
|
+
state: graph.initial_state,
|
|
30
|
+
message: "Workflow initial_state is not declared as a state.",
|
|
31
|
+
suggestion: "Declare an initial_state and ensure it is included in the workflow states."
|
|
32
|
+
)
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def state_reference_diagnostics
|
|
37
|
+
graph.transitions.values.flat_map do |transition|
|
|
38
|
+
[
|
|
39
|
+
undefined_state_diagnostic(transition, :from, transition.from),
|
|
40
|
+
undefined_state_diagnostic(transition, :to, transition.to)
|
|
41
|
+
].compact
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def undefined_state_diagnostic(transition, edge, state)
|
|
46
|
+
return if edge == :from && state.nil?
|
|
47
|
+
return if graph.states.include?(state)
|
|
48
|
+
|
|
49
|
+
Diagnostic.new(
|
|
50
|
+
severity: :error,
|
|
51
|
+
code: :"undefined_#{edge}_state",
|
|
52
|
+
state: state,
|
|
53
|
+
transition: transition.name,
|
|
54
|
+
message: "Transition #{ref(transition.name)} references undefined #{edge} state #{ref(state)}.",
|
|
55
|
+
suggestion: "Declare state #{ref(state)} or update transition #{ref(transition.name)}."
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ref(value)
|
|
60
|
+
Reference.format(value)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Targets
|
|
7
|
+
attr_reader :transition
|
|
8
|
+
|
|
9
|
+
def self.for(transition)
|
|
10
|
+
new(transition).names
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.router_for(transition)
|
|
14
|
+
new(transition).router_names
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(transition)
|
|
18
|
+
@transition = transition
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def names
|
|
22
|
+
names = [transition.success_transition, transition.failure_transition]
|
|
23
|
+
names.concat(router_names)
|
|
24
|
+
names.compact.uniq
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def router_names
|
|
28
|
+
return [] unless transition.router_config
|
|
29
|
+
|
|
30
|
+
[
|
|
31
|
+
*transition.router_config.fetch(:routes).values,
|
|
32
|
+
transition.router_config.fetch(:fallback)
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class TransitionDiagnostics
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_a
|
|
14
|
+
graph.transitions.values.flat_map do |transition|
|
|
15
|
+
[
|
|
16
|
+
transition_target_diagnostic(transition, :success_transition, transition.success_transition),
|
|
17
|
+
transition_target_diagnostic(transition, :failure_transition, transition.failure_transition),
|
|
18
|
+
*router_target_diagnostics(transition),
|
|
19
|
+
*target_state_mismatch_diagnostics(transition)
|
|
20
|
+
].compact
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def transition_target_diagnostic(transition, code, target)
|
|
27
|
+
return if target.nil?
|
|
28
|
+
return if graph.transitions.key?(target)
|
|
29
|
+
|
|
30
|
+
Diagnostic.new(
|
|
31
|
+
severity: :error,
|
|
32
|
+
code: :"unresolved_#{code}",
|
|
33
|
+
transition: transition.name,
|
|
34
|
+
target: target,
|
|
35
|
+
message: "Transition #{ref(transition.name)} references missing transition #{ref(target)}.",
|
|
36
|
+
suggestion: "Declare transition #{ref(target)} or update transition #{ref(transition.name)}."
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def router_target_diagnostics(transition)
|
|
41
|
+
return [] unless transition.router_config
|
|
42
|
+
|
|
43
|
+
Targets.router_for(transition).filter_map do |target|
|
|
44
|
+
transition_target_diagnostic(transition, :router_target, target)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def target_state_mismatch_diagnostics(transition)
|
|
49
|
+
Targets.for(transition).filter_map do |target_name|
|
|
50
|
+
target = graph.transitions[target_name]
|
|
51
|
+
next unless target
|
|
52
|
+
next if target.from.nil? || target.from == transition.to
|
|
53
|
+
|
|
54
|
+
mismatch_diagnostic(transition, target_name, target)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mismatch_diagnostic(transition, target_name, target)
|
|
59
|
+
Diagnostic.new(
|
|
60
|
+
severity: :warning,
|
|
61
|
+
code: :target_from_state_mismatch,
|
|
62
|
+
transition: transition.name,
|
|
63
|
+
target: target_name,
|
|
64
|
+
state: transition.to,
|
|
65
|
+
message: "Transition #{ref(transition.name)} can route to #{ref(target_name)}, " \
|
|
66
|
+
"but #{ref(target_name)} starts from #{ref(target.from)} instead of #{ref(transition.to)}.",
|
|
67
|
+
suggestion: "Align #{ref(target_name)}'s from state with #{ref(transition.to)}, " \
|
|
68
|
+
"or remove the named route."
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ref(value)
|
|
73
|
+
Reference.format(value)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class TransitionSnapshot
|
|
7
|
+
KINDS = [
|
|
8
|
+
%i[deterministic deterministic?],
|
|
9
|
+
%i[router routed?],
|
|
10
|
+
%i[nested_workflow nested?],
|
|
11
|
+
%i[optimizer optimized?],
|
|
12
|
+
%i[orchestrator orchestrated?],
|
|
13
|
+
%i[parallel parallel?]
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :name, :from, :to, :kind, :success_transition, :failure_transition, :routes, :fallback
|
|
17
|
+
|
|
18
|
+
def self.from_transition(transition)
|
|
19
|
+
new(
|
|
20
|
+
name: transition.name,
|
|
21
|
+
from: transition.from,
|
|
22
|
+
to: transition.to,
|
|
23
|
+
kind: kind_for(transition),
|
|
24
|
+
success_transition: transition.success_transition,
|
|
25
|
+
failure_transition: transition.failure_transition,
|
|
26
|
+
routes: transition.router_config&.fetch(:routes, nil),
|
|
27
|
+
fallback: transition.router_config&.fetch(:fallback, nil)
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.kind_for(transition)
|
|
32
|
+
kind = KINDS.find { |_name, predicate| transition.public_send(predicate) }
|
|
33
|
+
return kind.first if kind
|
|
34
|
+
return :agent if transition.agent_name
|
|
35
|
+
|
|
36
|
+
:noop
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(**attributes)
|
|
40
|
+
@name = attributes.fetch(:name)
|
|
41
|
+
@from = attributes.fetch(:from)
|
|
42
|
+
@to = attributes.fetch(:to)
|
|
43
|
+
@kind = attributes.fetch(:kind)
|
|
44
|
+
@success_transition = attributes[:success_transition]
|
|
45
|
+
@failure_transition = attributes[:failure_transition]
|
|
46
|
+
@routes = attributes[:routes]
|
|
47
|
+
@fallback = attributes[:fallback]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
{
|
|
52
|
+
name: name,
|
|
53
|
+
from: from,
|
|
54
|
+
to: to,
|
|
55
|
+
kind: kind,
|
|
56
|
+
success_transition: success_transition,
|
|
57
|
+
failure_transition: failure_transition,
|
|
58
|
+
routes: routes,
|
|
59
|
+
fallback: fallback
|
|
60
|
+
}.compact
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Validator
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def report
|
|
14
|
+
diagnostics = all_diagnostics
|
|
15
|
+
|
|
16
|
+
Report.new(
|
|
17
|
+
status: status_for(diagnostics),
|
|
18
|
+
workflow_class: graph.workflow_class.name,
|
|
19
|
+
initial_state: graph.initial_state,
|
|
20
|
+
states: graph.states,
|
|
21
|
+
transitions: graph.transition_snapshots,
|
|
22
|
+
diagnostics: diagnostics,
|
|
23
|
+
metrics: Metrics.new(graph, reachable_transition_names).to_h
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def all_diagnostics
|
|
30
|
+
[
|
|
31
|
+
*StateDiagnostics.new(graph).to_a,
|
|
32
|
+
*TransitionDiagnostics.new(graph).to_a,
|
|
33
|
+
*ReachabilityDiagnostics.new(graph, reachable_transition_names).to_a
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reachable_transition_names
|
|
38
|
+
@reachable_transition_names ||= Reachability.new(graph).transition_names
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def status_for(diagnostics)
|
|
42
|
+
return :invalid if diagnostics.any? { |diagnostic| diagnostic.severity == :error }
|
|
43
|
+
return :warning if diagnostics.any?
|
|
44
|
+
|
|
45
|
+
:valid
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
attr_reader :workflow_class, :initial_state, :states, :transitions
|
|
7
|
+
|
|
8
|
+
def initialize(workflow_class:, initial_state:, states:, transitions:)
|
|
9
|
+
@workflow_class = workflow_class
|
|
10
|
+
@initial_state = initial_state
|
|
11
|
+
@states = Array(states).uniq.freeze
|
|
12
|
+
@transitions = transitions.dup.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate
|
|
16
|
+
Validator.new(self).report
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def transition_snapshots
|
|
20
|
+
transitions.values.map { |transition| TransitionSnapshot.from_transition(transition) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require_relative "graph/reference"
|
|
27
|
+
require_relative "graph/diagnostic"
|
|
28
|
+
require_relative "graph/state_diagnostics"
|
|
29
|
+
require_relative "graph/reachability"
|
|
30
|
+
require_relative "graph/reachability_diagnostics"
|
|
31
|
+
require_relative "graph/metrics"
|
|
32
|
+
require_relative "graph/report"
|
|
33
|
+
require_relative "graph/targets"
|
|
34
|
+
require_relative "graph/transition_snapshot"
|
|
35
|
+
require_relative "graph/transition_diagnostics"
|
|
36
|
+
require_relative "graph/validator"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
def self.graph
|
|
6
|
+
Graph.new(
|
|
7
|
+
workflow_class: self,
|
|
8
|
+
initial_state: initial_state,
|
|
9
|
+
states: @states || [],
|
|
10
|
+
transitions: @transitions || {}
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.validate_graph
|
|
15
|
+
graph.validate
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/smith.rb
CHANGED
|
@@ -70,6 +70,7 @@ module Smith
|
|
|
70
70
|
unless %i[off auto].include?(value)
|
|
71
71
|
raise ArgumentError, "Smith.config.openai_api_mode must be :off or :auto, got #{value.inspect}"
|
|
72
72
|
end
|
|
73
|
+
|
|
73
74
|
value
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -227,6 +228,8 @@ require_relative "smith/agent/registry"
|
|
|
227
228
|
|
|
228
229
|
# Workflow (Transition, DSL, Persistence, and Execution must load before Workflow)
|
|
229
230
|
require_relative "smith/workflow/transition"
|
|
231
|
+
require_relative "smith/workflow/graph"
|
|
232
|
+
require_relative "smith/workflow/graph_dsl"
|
|
230
233
|
require_relative "smith/workflow/dsl"
|
|
231
234
|
require_relative "smith/workflow/persistence"
|
|
232
235
|
require_relative "smith/workflow/durability"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smith-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Ralak
|
|
@@ -219,6 +219,19 @@ files:
|
|
|
219
219
|
- lib/smith/workflow/event_integration.rb
|
|
220
220
|
- lib/smith/workflow/execution.rb
|
|
221
221
|
- lib/smith/workflow/execution_frame.rb
|
|
222
|
+
- lib/smith/workflow/graph.rb
|
|
223
|
+
- lib/smith/workflow/graph/diagnostic.rb
|
|
224
|
+
- lib/smith/workflow/graph/metrics.rb
|
|
225
|
+
- lib/smith/workflow/graph/reachability.rb
|
|
226
|
+
- lib/smith/workflow/graph/reachability_diagnostics.rb
|
|
227
|
+
- lib/smith/workflow/graph/reference.rb
|
|
228
|
+
- lib/smith/workflow/graph/report.rb
|
|
229
|
+
- lib/smith/workflow/graph/state_diagnostics.rb
|
|
230
|
+
- lib/smith/workflow/graph/targets.rb
|
|
231
|
+
- lib/smith/workflow/graph/transition_diagnostics.rb
|
|
232
|
+
- lib/smith/workflow/graph/transition_snapshot.rb
|
|
233
|
+
- lib/smith/workflow/graph/validator.rb
|
|
234
|
+
- lib/smith/workflow/graph_dsl.rb
|
|
222
235
|
- lib/smith/workflow/guardrail_integration.rb
|
|
223
236
|
- lib/smith/workflow/nested_execution.rb
|
|
224
237
|
- lib/smith/workflow/orchestrator_worker.rb
|