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
|
@@ -36,29 +36,40 @@ module Smith
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Walk Smith::Agent::Registry. For each agent, extract
|
|
40
|
-
# id
|
|
41
|
-
#
|
|
42
|
-
#
|
|
39
|
+
# Walk Smith::Agent::Registry. For each agent, extract every static
|
|
40
|
+
# model id Smith can know at boot: the primary `model "..."` value and
|
|
41
|
+
# any static fallback models. Block-form primary models are skipped
|
|
42
|
+
# because they resolve per-attempt, but their static fallbacks still
|
|
43
|
+
# need coverage checks.
|
|
43
44
|
# Check whether find_or_infer returns a custom (non-default)
|
|
44
45
|
# Profile — meaning either an explicit override or an inference
|
|
45
46
|
# rule matched.
|
|
46
47
|
def uncovered_models
|
|
47
48
|
return [] unless defined?(Smith::Agent::Registry)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
next unless agent.is_a?(Class)
|
|
52
|
-
next unless agent.respond_to?(:chat_kwargs)
|
|
50
|
+
static_model_ids.uniq.reject { |model_id| covered_model?(model_id) }
|
|
51
|
+
end
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
def static_model_ids
|
|
54
|
+
Smith::Agent::Registry.each.with_object([]) do |(_key, agent), ids|
|
|
55
|
+
ids.concat(static_model_ids_for(agent)) if inspectable_agent?(agent)
|
|
56
56
|
end
|
|
57
|
+
end
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
def inspectable_agent?(agent)
|
|
60
|
+
agent.is_a?(Class) && agent.respond_to?(:chat_kwargs)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def static_model_ids_for(agent)
|
|
64
|
+
[
|
|
65
|
+
agent.chat_kwargs[:model],
|
|
66
|
+
*(agent.respond_to?(:fallback_models) ? agent.fallback_models : nil)
|
|
67
|
+
].compact
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def covered_model?(model_id)
|
|
71
|
+
Smith::Models.find(model_id) ||
|
|
72
|
+
(defined?(Smith::Models::Inference) && Smith::Models::Inference.profile_for(model_id))
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../persistence_adapters"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
module Doctor
|
|
5
7
|
module Checks
|
|
@@ -12,41 +14,16 @@ module Smith
|
|
|
12
14
|
module PersistenceCapabilities
|
|
13
15
|
module_function
|
|
14
16
|
|
|
15
|
-
OPTIONAL_CAPABILITIES =
|
|
17
|
+
OPTIONAL_CAPABILITIES = Smith::PersistenceAdapters::OPTIONAL_METHODS
|
|
16
18
|
|
|
17
19
|
def run(report)
|
|
18
20
|
adapter = resolve_adapter
|
|
19
|
-
if adapter.nil?
|
|
20
|
-
report.add(
|
|
21
|
-
name: "persistence.capabilities",
|
|
22
|
-
status: :warn,
|
|
23
|
-
message: "No persistence adapter configured",
|
|
24
|
-
detail: "Smith.config.persistence_adapter is nil and Smith.config.test_mode is false. " \
|
|
25
|
-
"Hosts using durable workflows must set persistence_adapter."
|
|
26
|
-
)
|
|
27
|
-
return
|
|
28
|
-
end
|
|
21
|
+
return report_missing_adapter(report) if adapter.nil?
|
|
29
22
|
|
|
30
23
|
missing = OPTIONAL_CAPABILITIES.reject { |cap| Smith::PersistenceAdapters.supports?(adapter, cap) }
|
|
24
|
+
return report_supported_capabilities(report, adapter) if missing.empty?
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
report.add(
|
|
34
|
-
name: "persistence.capabilities",
|
|
35
|
-
status: :pass,
|
|
36
|
-
message: "#{adapter.class.name} supports all optional persistence capabilities",
|
|
37
|
-
detail: "Supported: #{OPTIONAL_CAPABILITIES.join(', ')}"
|
|
38
|
-
)
|
|
39
|
-
else
|
|
40
|
-
report.add(
|
|
41
|
-
name: "persistence.capabilities",
|
|
42
|
-
status: :warn,
|
|
43
|
-
message: "#{adapter.class.name} missing optional capabilities: #{missing.join(', ')}",
|
|
44
|
-
detail: "Workflows using these capabilities fall back to non-versioned writes " \
|
|
45
|
-
"with a one-time warning per adapter class. Switch to RedisStore, " \
|
|
46
|
-
"ActiveRecordStore (with lock_version column), or the Memory adapter " \
|
|
47
|
-
"for full coverage."
|
|
48
|
-
)
|
|
49
|
-
end
|
|
26
|
+
report_missing_capabilities(report, adapter, missing)
|
|
50
27
|
end
|
|
51
28
|
|
|
52
29
|
def resolve_adapter
|
|
@@ -54,6 +31,37 @@ module Smith
|
|
|
54
31
|
rescue StandardError
|
|
55
32
|
nil
|
|
56
33
|
end
|
|
34
|
+
|
|
35
|
+
def report_missing_adapter(report)
|
|
36
|
+
report.add(
|
|
37
|
+
name: "persistence.capabilities",
|
|
38
|
+
status: :warn,
|
|
39
|
+
message: "No persistence adapter configured",
|
|
40
|
+
detail: "Smith.config.persistence_adapter is nil and Smith.config.test_mode is false. " \
|
|
41
|
+
"Hosts using durable workflows must set persistence_adapter."
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def report_supported_capabilities(report, adapter)
|
|
46
|
+
report.add(
|
|
47
|
+
name: "persistence.capabilities",
|
|
48
|
+
status: :pass,
|
|
49
|
+
message: "#{adapter.class.name} supports all optional persistence capabilities",
|
|
50
|
+
detail: "Supported: #{OPTIONAL_CAPABILITIES.join(", ")}"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def report_missing_capabilities(report, adapter, missing)
|
|
55
|
+
report.add(
|
|
56
|
+
name: "persistence.capabilities",
|
|
57
|
+
status: :warn,
|
|
58
|
+
message: "#{adapter.class.name} missing optional capabilities: #{missing.join(", ")}",
|
|
59
|
+
detail: "Smith will fall back where possible: non-versioned writes when store_versioned " \
|
|
60
|
+
"is missing, and payload updated_at parsing when heartbeat methods are missing. " \
|
|
61
|
+
"Use RedisStore or Memory for full versioning and heartbeat coverage; " \
|
|
62
|
+
"ActiveRecordStore currently covers optimistic locking when lock_version is present."
|
|
63
|
+
)
|
|
64
|
+
end
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
end
|
data/lib/smith/version.rb
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
# rubocop:disable Style/RedundantStructKeywordInit
|
|
6
|
+
AgentResult = Struct.new(
|
|
7
|
+
:content, :input_tokens, :output_tokens, :cost, :model_used,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
) do
|
|
10
|
+
def self.from_response(response, content, model_used: nil)
|
|
11
|
+
new(
|
|
12
|
+
content: content,
|
|
13
|
+
input_tokens: response.respond_to?(:input_tokens) ? response.input_tokens : nil,
|
|
14
|
+
output_tokens: response.respond_to?(:output_tokens) ? response.output_tokens : nil,
|
|
15
|
+
cost: nil,
|
|
16
|
+
model_used: model_used
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def usage_known?
|
|
21
|
+
!input_tokens.nil? && !output_tokens.nil?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
# rubocop:enable Style/RedundantStructKeywordInit
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
# rubocop:disable Style/RedundantStructKeywordInit
|
|
6
|
+
BranchEnv = Struct.new(
|
|
7
|
+
:prepared_input, :guardrail_sources, :scoped_store, :branch_estimates, :deadline,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
) do
|
|
10
|
+
def setup_thread
|
|
11
|
+
Smith::Tool.current_guardrails = guardrail_sources
|
|
12
|
+
Smith::Tool.current_deadline = deadline
|
|
13
|
+
Smith.scoped_artifacts = scoped_store
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def teardown_thread
|
|
17
|
+
Smith::Tool.current_guardrails = nil
|
|
18
|
+
Smith::Tool.current_deadline = nil
|
|
19
|
+
Smith.scoped_artifacts = nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
# rubocop:enable Style/RedundantStructKeywordInit
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "agent_result"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
class Workflow
|
|
5
7
|
module BudgetIntegration
|
|
@@ -30,19 +32,33 @@ module Smith
|
|
|
30
32
|
def reconcile_branch_budget(ledger, estimates, agent_result: nil)
|
|
31
33
|
return unless ledger && estimates
|
|
32
34
|
|
|
33
|
-
actuals = extract_actuals(agent_result)
|
|
35
|
+
actuals = extract_actuals(agent_results_for_settlement(agent_result))
|
|
34
36
|
estimates.each do |dim, amt|
|
|
35
37
|
ledger.reconcile!(dim, amt, actual_for_dimension(dim, actuals[:tokens], actuals[:cost]))
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
def extract_actuals(
|
|
41
|
+
def extract_actuals(agent_results)
|
|
42
|
+
results = Array(agent_results).compact
|
|
43
|
+
|
|
40
44
|
{
|
|
41
|
-
tokens: (
|
|
42
|
-
cost:
|
|
45
|
+
tokens: results.sum { |result| (result.input_tokens || 0) + (result.output_tokens || 0) },
|
|
46
|
+
cost: results.sum { |result| result.cost || 0 }
|
|
43
47
|
}
|
|
44
48
|
end
|
|
45
49
|
|
|
50
|
+
def agent_results_for_settlement(agent_result = nil)
|
|
51
|
+
[*failed_billable_attempts, agent_result].compact
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def failed_billable_attempts
|
|
55
|
+
Array(Thread.current[:smith_failed_agent_results])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def clear_failed_billable_attempts
|
|
59
|
+
Thread.current[:smith_failed_agent_results] = []
|
|
60
|
+
end
|
|
61
|
+
|
|
46
62
|
def actual_for_dimension(dim, actual_tokens, actual_cost = 0)
|
|
47
63
|
return actual_tokens if TOKEN_DIMENSIONS.include?(dim)
|
|
48
64
|
return actual_cost if COST_DIMENSIONS.include?(dim)
|
|
@@ -59,7 +75,7 @@ module Smith
|
|
|
59
75
|
def settle_budget_on_failure(ledger, estimates, agent_result)
|
|
60
76
|
return unless ledger && estimates
|
|
61
77
|
|
|
62
|
-
if agent_result
|
|
78
|
+
if agent_result || failed_billable_attempts.any?
|
|
63
79
|
reconcile_branch_budget(ledger, estimates, agent_result: agent_result)
|
|
64
80
|
else
|
|
65
81
|
release_branch_budget(ledger, estimates)
|
|
@@ -85,6 +101,12 @@ module Smith
|
|
|
85
101
|
{ branch: index, agent: transition.agent_name, output: agent_result ? agent_result.content : result }
|
|
86
102
|
end
|
|
87
103
|
|
|
104
|
+
def finalize_named_branch(branch_key, agent_name, result, ledger, reserved)
|
|
105
|
+
agent_result = result.is_a?(Workflow::AgentResult) ? result : nil
|
|
106
|
+
reconcile_branch_budget(ledger, reserved, agent_result: agent_result)
|
|
107
|
+
{ branch: branch_key, agent: agent_name, output: agent_result ? agent_result.content : result }
|
|
108
|
+
end
|
|
109
|
+
|
|
88
110
|
def estimate_for_dimension(dim, limit, branch_count)
|
|
89
111
|
return 0 unless BUDGET_DIMENSIONS.include?(dim)
|
|
90
112
|
|
|
@@ -4,14 +4,17 @@ module Smith
|
|
|
4
4
|
class Workflow
|
|
5
5
|
class DeterministicStep
|
|
6
6
|
attr_reader :context, :tool_results, :session_messages, :current_state, :transition_name,
|
|
7
|
-
:context_writes, :routed_to, :outcome
|
|
7
|
+
:context_writes, :routed_to, :outcome, :allowed_routes
|
|
8
8
|
|
|
9
|
-
def initialize(context:, session_messages:, tool_results:, state:,
|
|
9
|
+
def initialize(context:, session_messages:, tool_results:, state:, **options)
|
|
10
|
+
validate_options!(options)
|
|
11
|
+
transition = options[:transition]
|
|
10
12
|
@context = context
|
|
11
13
|
@session_messages = session_messages
|
|
12
14
|
@tool_results = tool_results
|
|
13
15
|
@current_state = state
|
|
14
|
-
@transition_name = transition_name
|
|
16
|
+
@transition_name = transition ? transition.name : options.fetch(:transition_name)
|
|
17
|
+
@allowed_routes = snapshot_allowed_routes(transition ? transition.deterministic_routes : options[:allowed_routes])
|
|
15
18
|
@context_writes = {}
|
|
16
19
|
@routed_to = nil
|
|
17
20
|
@outcome = nil
|
|
@@ -39,7 +42,7 @@ module Smith
|
|
|
39
42
|
def route_to(transition_name)
|
|
40
43
|
raise WorkflowError, "route_to already called with :#{@routed_to}" if @routed_to
|
|
41
44
|
|
|
42
|
-
@routed_to = transition_name
|
|
45
|
+
@routed_to = route_target_for(transition_name)
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def write_outcome(kind:, payload:)
|
|
@@ -52,6 +55,31 @@ module Smith
|
|
|
52
55
|
def fail!(message, retryable: nil, kind: nil, details: nil)
|
|
53
56
|
raise DeterministicStepFailure.new(message, retryable: retryable, kind: kind, details: details)
|
|
54
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def validate_options!(options)
|
|
62
|
+
unknown = options.keys - %i[transition transition_name allowed_routes]
|
|
63
|
+
raise ArgumentError, "unknown keywords: #{unknown.join(", ")}" if unknown.any?
|
|
64
|
+
return if options[:transition] || options.key?(:transition_name)
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, "missing keyword: :transition"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def snapshot_allowed_routes(routes)
|
|
70
|
+
return nil if routes.nil?
|
|
71
|
+
|
|
72
|
+
routes.map { |route| route.is_a?(String) ? route.dup.freeze : route }.freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def route_target_for(transition_name)
|
|
76
|
+
return transition_name if allowed_routes.nil?
|
|
77
|
+
|
|
78
|
+
allowed_route = allowed_routes.find { |route| route == transition_name }
|
|
79
|
+
return allowed_route if allowed_route
|
|
80
|
+
|
|
81
|
+
raise WorkflowError, "route_to #{transition_name.inspect} is not declared in deterministic routes"
|
|
82
|
+
end
|
|
55
83
|
end
|
|
56
84
|
end
|
|
57
85
|
end
|
|
@@ -32,11 +32,9 @@ module Smith
|
|
|
32
32
|
# `store/fetch/delete`, and this peek piggybacks on `fetch`.
|
|
33
33
|
# Custom adapters work without changes.
|
|
34
34
|
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# in-flight workflow is being resumed — either way, no NEW
|
|
39
|
-
# credit authorization is needed).
|
|
35
|
+
# Hosts can use this to distinguish a resumed workflow from a
|
|
36
|
+
# brand-new workflow when coordinating external accounting or
|
|
37
|
+
# admission-control checks.
|
|
40
38
|
def persisted_state_exists?(key: nil, context: {}, adapter: Smith.persistence_adapter)
|
|
41
39
|
resolved_key = resolved_persistence_key(key:, context:)
|
|
42
40
|
!fetch_persisted_payload(resolved_key, adapter:).nil?
|
|
@@ -51,11 +49,9 @@ module Smith
|
|
|
51
49
|
# the top of `run_persisted!` BEFORE the first `advance!`. A
|
|
52
50
|
# worker that dies between that initial `persist!` and the
|
|
53
51
|
# first model call leaves a Redis key with no billable work.
|
|
54
|
-
# If
|
|
55
|
-
# alone, a
|
|
56
|
-
# state
|
|
57
|
-
# skipped because state exists, but the state has nothing to
|
|
58
|
-
# bill — it's just the workflow's starting state).
|
|
52
|
+
# If external accounting or admission-control checks key on
|
|
53
|
+
# `persisted_state_exists?` alone, a retry on that abandoned
|
|
54
|
+
# init state can be mistaken for a billable/resumable workflow.
|
|
59
55
|
#
|
|
60
56
|
# `restorable_billing_state?` returns true only when there's
|
|
61
57
|
# actual `usage_entries` to bill on idempotent replay. Terminal
|
|
@@ -167,11 +163,22 @@ module Smith
|
|
|
167
163
|
return false unless state_name && class_name
|
|
168
164
|
|
|
169
165
|
klass = Object.const_get(class_name)
|
|
170
|
-
klass
|
|
166
|
+
state = state_name_for_payload(klass, state_name)
|
|
167
|
+
klass.transitions_from(state).empty? && next_transition.nil?
|
|
171
168
|
rescue JSON::ParserError, NameError, NoMethodError
|
|
172
169
|
false
|
|
173
170
|
end
|
|
174
171
|
|
|
172
|
+
def state_name_for_payload(klass, state_name)
|
|
173
|
+
states = klass.instance_variable_get(:@states) || []
|
|
174
|
+
return state_name if states.include?(state_name)
|
|
175
|
+
|
|
176
|
+
symbolized = state_name.to_sym if state_name.respond_to?(:to_sym)
|
|
177
|
+
return symbolized if states.include?(symbolized)
|
|
178
|
+
|
|
179
|
+
state_name
|
|
180
|
+
end
|
|
181
|
+
|
|
175
182
|
def persistence_adapter!(adapter)
|
|
176
183
|
return adapter if adapter
|
|
177
184
|
|
|
@@ -241,12 +248,19 @@ module Smith
|
|
|
241
248
|
persist!(resolved_key, adapter:)
|
|
242
249
|
|
|
243
250
|
until terminal?
|
|
251
|
+
ensure_transition_budget!
|
|
252
|
+
|
|
244
253
|
if strict_idempotency?
|
|
245
254
|
mark_step_in_progress!
|
|
246
255
|
persist!(resolved_key, adapter:)
|
|
247
256
|
end
|
|
248
257
|
|
|
249
|
-
|
|
258
|
+
begin
|
|
259
|
+
step = advance!
|
|
260
|
+
rescue StandardError
|
|
261
|
+
clear_pre_step_marker!(resolved_key, adapter:)
|
|
262
|
+
raise
|
|
263
|
+
end
|
|
250
264
|
steps << step if step
|
|
251
265
|
|
|
252
266
|
clear_step_in_progress!
|
|
@@ -261,6 +275,7 @@ module Smith
|
|
|
261
275
|
return if terminal?
|
|
262
276
|
|
|
263
277
|
resolved_key = resolve_persistence_key!(key)
|
|
278
|
+
ensure_transition_budget!
|
|
264
279
|
mark_step_in_progress! if strict_idempotency?
|
|
265
280
|
persist!(resolved_key, adapter:)
|
|
266
281
|
step = advance!
|
|
@@ -268,6 +283,9 @@ module Smith
|
|
|
268
283
|
persist!(resolved_key, adapter:) if step
|
|
269
284
|
invoke_on_step_callback(step, on_step) if step
|
|
270
285
|
step
|
|
286
|
+
rescue StandardError
|
|
287
|
+
clear_pre_step_marker!(resolved_key, adapter:) if defined?(resolved_key) && resolved_key
|
|
288
|
+
raise
|
|
271
289
|
end
|
|
272
290
|
|
|
273
291
|
def persist!(key = nil, adapter: Smith.persistence_adapter)
|
|
@@ -306,6 +324,14 @@ module Smith
|
|
|
306
324
|
self.class.idempotency_mode == :strict
|
|
307
325
|
end
|
|
308
326
|
|
|
327
|
+
def clear_pre_step_marker!(key, adapter:)
|
|
328
|
+
return unless strict_idempotency?
|
|
329
|
+
return if step_work_started?
|
|
330
|
+
|
|
331
|
+
clear_step_in_progress!
|
|
332
|
+
persist!(key, adapter:)
|
|
333
|
+
end
|
|
334
|
+
|
|
309
335
|
# Forwards the persist payload to the adapter, splatting `ttl:`
|
|
310
336
|
# only when a TTL is resolved. The empty-Hash splat is a no-op so
|
|
311
337
|
# external duck-typed adapters that don't accept a `ttl:` kwarg
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "agent_result"
|
|
6
|
+
require_relative "optimization_state"
|
|
7
|
+
|
|
3
8
|
module Smith
|
|
4
9
|
class Workflow
|
|
5
10
|
module EvaluatorOptimizer
|
|
6
|
-
OptimizationState = Struct.new(
|
|
7
|
-
:config, :prepared_input, :candidate, :feedback, :last_score, :generator_class, :evaluator_class
|
|
8
|
-
) do
|
|
9
|
-
def initialize(config, prepared_input)
|
|
10
|
-
super(config, prepared_input, nil, nil, nil, nil, nil)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
11
|
private
|
|
15
12
|
|
|
16
13
|
def execute_optimization_step(transition, prepared_input: nil)
|
|
@@ -91,7 +88,7 @@ module Smith
|
|
|
91
88
|
when Hash
|
|
92
89
|
deep_symbolize_evaluation(evaluation)
|
|
93
90
|
when String
|
|
94
|
-
parsed = (
|
|
91
|
+
parsed = parse_evaluation_json(evaluation)
|
|
95
92
|
parsed.is_a?(Hash) ? parsed : evaluation
|
|
96
93
|
else
|
|
97
94
|
evaluation
|
|
@@ -155,9 +152,12 @@ module Smith
|
|
|
155
152
|
|
|
156
153
|
def invoke_agent_with_budget(agent_class, prepared_input)
|
|
157
154
|
Thread.current[:smith_last_agent_result] = nil
|
|
155
|
+
clear_failed_billable_attempts
|
|
158
156
|
with_agent_context(agent_class) do
|
|
159
157
|
invoke_with_call_ledger(agent_class, prepared_input)
|
|
160
158
|
end
|
|
159
|
+
ensure
|
|
160
|
+
clear_failed_billable_attempts
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
def invoke_with_call_ledger(agent_class, prepared_input)
|
|
@@ -179,12 +179,20 @@ module Smith
|
|
|
179
179
|
# routes through on_threshold and returns the resulting value
|
|
180
180
|
# (non-nil terminates the loop with that as the step output).
|
|
181
181
|
def check_improvement_threshold!(evaluation, state, round)
|
|
182
|
-
|
|
182
|
+
unless stop_for_threshold?(evaluation[:score], state.last_score, state.config[:improvement_threshold])
|
|
183
|
+
return nil
|
|
184
|
+
end
|
|
183
185
|
|
|
184
186
|
handle_exit(state, :on_threshold,
|
|
185
187
|
"optimization improvement below threshold after round #{round + 1}")
|
|
186
188
|
end
|
|
187
189
|
|
|
190
|
+
def parse_evaluation_json(evaluation)
|
|
191
|
+
JSON.parse(evaluation, symbolize_names: true)
|
|
192
|
+
rescue JSON::ParserError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
188
196
|
def prepare_generator_input(prepared_input, round, prior_candidate, feedback)
|
|
189
197
|
return prepared_input if round.zero?
|
|
190
198
|
|
|
@@ -13,9 +13,9 @@ module Smith
|
|
|
13
13
|
|
|
14
14
|
Smith::Events.emit(
|
|
15
15
|
Events::StepCompleted.new(
|
|
16
|
-
transition: transition.name,
|
|
17
|
-
from: transition.from,
|
|
18
|
-
to: transition.to
|
|
16
|
+
transition: transition.name.to_sym,
|
|
17
|
+
from: transition.from&.to_sym,
|
|
18
|
+
to: transition.to.to_sym
|
|
19
19
|
)
|
|
20
20
|
)
|
|
21
21
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "agent_result"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
class Workflow
|
|
5
7
|
module Execution
|
|
@@ -8,13 +10,15 @@ module Smith
|
|
|
8
10
|
include EvaluatorOptimizer
|
|
9
11
|
include OrchestratorWorker
|
|
10
12
|
include ParallelExecution
|
|
13
|
+
include FanoutExecution
|
|
14
|
+
include RetryExecution
|
|
11
15
|
include DeterministicExecution
|
|
12
16
|
|
|
13
17
|
private
|
|
14
18
|
|
|
15
19
|
def execute_step(transition)
|
|
16
20
|
setup_step_context
|
|
17
|
-
output = with_scoped_artifacts {
|
|
21
|
+
output = with_scoped_artifacts { run_with_retry_policy(transition) }
|
|
18
22
|
complete_step(transition, output)
|
|
19
23
|
rescue StandardError => e
|
|
20
24
|
@outcome = nil
|
|
@@ -40,6 +44,7 @@ module Smith
|
|
|
40
44
|
|
|
41
45
|
def run_guarded_step(transition)
|
|
42
46
|
return dispatch_step(transition) if transition.deterministic?
|
|
47
|
+
return run_guarded_fanout_step(transition) if transition.fanout?
|
|
43
48
|
|
|
44
49
|
agent_class = resolve_agent_class(transition)
|
|
45
50
|
run_input_guardrails(agent_class)
|
|
@@ -93,6 +98,7 @@ module Smith
|
|
|
93
98
|
|
|
94
99
|
def execute_serial_step(transition, prepared_input: nil)
|
|
95
100
|
Thread.current[:smith_last_agent_result] = nil
|
|
101
|
+
clear_failed_billable_attempts
|
|
96
102
|
ledger = effective_call_ledger
|
|
97
103
|
reserved = reserve_for_serial(transition, ledger)
|
|
98
104
|
begin
|
|
@@ -104,6 +110,7 @@ module Smith
|
|
|
104
110
|
ensure
|
|
105
111
|
settle_budget_on_failure(ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
|
|
106
112
|
Thread.current[:smith_last_agent_result] = nil
|
|
113
|
+
clear_failed_billable_attempts
|
|
107
114
|
end
|
|
108
115
|
end
|
|
109
116
|
|