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
|
@@ -6,34 +6,69 @@ module Smith
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
8
|
def apply_tool_guardrails(agent_class)
|
|
9
|
-
sources =
|
|
9
|
+
sources = tool_guardrail_sources(agent_class)
|
|
10
10
|
Tool.current_guardrails = sources.empty? ? nil : sources
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def run_input_guardrails(agent_class)
|
|
14
|
+
run_workflow_input_guardrails
|
|
15
|
+
run_agent_input_guardrails(agent_class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run_output_guardrails(output, agent_class)
|
|
19
|
+
run_workflow_output_guardrails(output)
|
|
20
|
+
run_agent_output_guardrails(output, agent_class)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def handle_step_failure(transition, error)
|
|
24
|
+
failure_name = transition.failure_transition
|
|
25
|
+
raise error unless failure_name
|
|
26
|
+
|
|
27
|
+
fail_transition = self.class.find_transition(failure_name)
|
|
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
|
|
35
|
+
|
|
36
|
+
@state = fail_transition.to
|
|
37
|
+
end
|
|
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
|
+
|
|
50
|
+
def run_workflow_input_guardrails
|
|
14
51
|
wf_guardrails = self.class.guardrails
|
|
15
52
|
Guardrails::Runner.run_inputs(wf_guardrails, @context) if wf_guardrails
|
|
53
|
+
end
|
|
16
54
|
|
|
55
|
+
def run_agent_input_guardrails(agent_class)
|
|
17
56
|
agent_guardrails = agent_class&.guardrails
|
|
18
57
|
Guardrails::Runner.run_inputs(agent_guardrails, @context) if agent_guardrails
|
|
19
58
|
end
|
|
20
59
|
|
|
21
|
-
def
|
|
60
|
+
def run_workflow_output_guardrails(output)
|
|
22
61
|
wf_guardrails = self.class.guardrails
|
|
23
62
|
Guardrails::Runner.run_outputs(wf_guardrails, output) if wf_guardrails
|
|
63
|
+
end
|
|
24
64
|
|
|
65
|
+
def run_agent_output_guardrails(output, agent_class)
|
|
25
66
|
agent_guardrails = agent_class&.guardrails
|
|
26
67
|
Guardrails::Runner.run_outputs(agent_guardrails, output) if agent_guardrails
|
|
27
68
|
end
|
|
28
69
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
return unless failure_name
|
|
32
|
-
|
|
33
|
-
fail_transition = self.class.find_transition(failure_name)
|
|
34
|
-
return unless fail_transition
|
|
35
|
-
|
|
36
|
-
@state = fail_transition.to
|
|
70
|
+
def tool_guardrail_sources(agent_class)
|
|
71
|
+
[self.class.guardrails, agent_class&.guardrails].compact
|
|
37
72
|
end
|
|
38
73
|
end
|
|
39
74
|
end
|
|
@@ -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,29 +1,18 @@
|
|
|
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)
|
|
26
|
-
if transition.
|
|
14
|
+
if transition.fanout? then execute_fanout_step(transition, prepared_input: prepared_input)
|
|
15
|
+
elsif transition.parallel? then execute_parallel_step(transition, prepared_input: prepared_input)
|
|
27
16
|
elsif transition.nested? then execute_nested_workflow(transition)
|
|
28
17
|
elsif transition.optimized? then execute_optimization_step(transition, prepared_input: prepared_input)
|
|
29
18
|
elsif transition.orchestrated? then execute_orchestration_step(transition, prepared_input: prepared_input)
|
|
@@ -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:)
|
|
@@ -39,12 +30,17 @@ module Smith
|
|
|
39
30
|
fulfilled, values, reasons = Concurrent::Promises.zip(*futures).result
|
|
40
31
|
|
|
41
32
|
unless fulfilled
|
|
42
|
-
error = reasons
|
|
33
|
+
error = preferred_error(reasons)
|
|
43
34
|
raise error
|
|
44
35
|
end
|
|
45
36
|
|
|
46
37
|
values
|
|
47
38
|
end
|
|
39
|
+
|
|
40
|
+
def self.preferred_error(reasons)
|
|
41
|
+
errors = reasons.compact
|
|
42
|
+
errors.find { |error| !error.is_a?(Cancellation) } || errors.first
|
|
43
|
+
end
|
|
48
44
|
end
|
|
49
45
|
end
|
|
50
46
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "branch_env"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
class Workflow
|
|
5
7
|
module ParallelExecution
|
|
@@ -50,10 +52,12 @@ module Smith
|
|
|
50
52
|
Tool.current_ledger = ledger
|
|
51
53
|
Tool.current_tool_result_collector = tool_result_collector
|
|
52
54
|
Thread.current[:smith_last_agent_result] = nil
|
|
55
|
+
clear_failed_billable_attempts
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
def teardown_branch_context(env)
|
|
56
59
|
Thread.current[:smith_last_agent_result] = nil
|
|
60
|
+
clear_failed_billable_attempts
|
|
57
61
|
Tool.current_ledger = nil
|
|
58
62
|
Tool.current_tool_result_collector = nil
|
|
59
63
|
env.teardown_thread
|
|
@@ -68,7 +72,7 @@ module Smith
|
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def check_cancellation!(signal)
|
|
71
|
-
raise
|
|
75
|
+
raise Parallel::Cancellation, "cancelled" if signal.cancelled?
|
|
72
76
|
end
|
|
73
77
|
end
|
|
74
78
|
end
|
|
@@ -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)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module RetryExecution
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def run_with_retry_policy(transition)
|
|
9
|
+
config = transition.retry_config
|
|
10
|
+
return run_guarded_step(transition) unless config
|
|
11
|
+
|
|
12
|
+
attempt = 0
|
|
13
|
+
begin
|
|
14
|
+
attempt += 1
|
|
15
|
+
run_guarded_step(transition)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
raise unless retry_transition_error?(config, e, attempt)
|
|
18
|
+
|
|
19
|
+
sleep_for_retry(config, attempt)
|
|
20
|
+
retry
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def retry_transition_error?(config, error, attempt)
|
|
25
|
+
return false if attempt >= config.fetch(:attempts)
|
|
26
|
+
|
|
27
|
+
classes = config.fetch(:error_classes)
|
|
28
|
+
if classes.any?
|
|
29
|
+
classes.any? { |error_class| error.is_a?(error_class) }
|
|
30
|
+
else
|
|
31
|
+
Smith::Errors.retryable?(error)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sleep_for_retry(config, failed_attempt)
|
|
36
|
+
delay = retry_delay(config, failed_attempt)
|
|
37
|
+
sleep(delay) if delay.positive?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def retry_delay(config, failed_attempt)
|
|
41
|
+
delay = config.fetch(:backoff) * (2**[failed_attempt - 1, 0].max)
|
|
42
|
+
max_delay = config[:max_delay]
|
|
43
|
+
delay = [delay, max_delay].min if max_delay
|
|
44
|
+
|
|
45
|
+
jitter = config.fetch(:jitter)
|
|
46
|
+
delay += rand * jitter if jitter.positive?
|
|
47
|
+
delay = [delay, max_delay].min if max_delay
|
|
48
|
+
delay
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -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
|