smith-agents 0.11.0 → 0.12.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 +712 -0
- data/README.md +622 -15
- data/UPSTREAM_PROPOSAL.md +1 -0
- data/docs/CONFIGURATION.md +80 -14
- data/docs/PATTERNS.md +30 -13
- data/docs/PERSISTENCE.md +733 -5
- data/docs/TOOLS_AND_GUARDRAILS.md +24 -3
- data/lib/generators/smith/install/install_generator.rb +67 -2
- data/lib/generators/smith/install/templates/create_workflow_states.rb.tt +31 -0
- data/lib/generators/smith/install/templates/smith.rb.tt +39 -8
- data/lib/generators/smith/install/templates/workflow_state.rb.tt +19 -0
- data/lib/smith/agent/completion.rb +32 -6
- data/lib/smith/agent/completion_usage_recording.rb +23 -14
- data/lib/smith/agent/content_bytes.rb +98 -0
- data/lib/smith/agent/content_parts.rb +149 -0
- data/lib/smith/agent/invocation_preparation.rb +37 -14
- data/lib/smith/agent/lifecycle.rb +16 -5
- data/lib/smith/agent/observed_model.rb +29 -0
- data/lib/smith/agent/output_contract_check.rb +53 -0
- data/lib/smith/agent/output_integrity.rb +137 -0
- data/lib/smith/agent/output_policy.rb +92 -0
- data/lib/smith/agent/output_recovery.rb +129 -0
- data/lib/smith/agent/provider_attempt.rb +6 -0
- data/lib/smith/agent/provider_call_timing.rb +13 -4
- data/lib/smith/agent/provider_completion.rb +35 -32
- data/lib/smith/agent/provider_failure_handling.rb +5 -1
- data/lib/smith/agent/provider_request.rb +36 -0
- data/lib/smith/agent/provider_streaming.rb +155 -0
- data/lib/smith/agent/provider_usage.rb +70 -3
- data/lib/smith/agent/usage_entry_recording.rb +4 -2
- data/lib/smith/agent/usage_traces.rb +9 -2
- data/lib/smith/agent/usage_tracking.rb +64 -51
- data/lib/smith/agent.rb +5 -0
- data/lib/smith/artifacts/active_record_store.rb +79 -0
- data/lib/smith/artifacts/file.rb +45 -18
- data/lib/smith/artifacts/memory.rb +47 -36
- data/lib/smith/artifacts/s3.rb +126 -0
- data/lib/smith/artifacts/scoped_store.rb +55 -7
- data/lib/smith/artifacts/store_rules.rb +49 -0
- data/lib/smith/artifacts.rb +98 -1
- data/lib/smith/batch/message_shape.rb +36 -0
- data/lib/smith/batch.rb +166 -0
- data/lib/smith/budget/amount.rb +25 -0
- data/lib/smith/budget/continuation.rb +153 -0
- data/lib/smith/budget/host_ledger.rb +137 -0
- data/lib/smith/budget/ledger.rb +4 -2
- data/lib/smith/budget/ledger_state_transition.rb +7 -5
- data/lib/smith/budget/request_hold.rb +211 -0
- data/lib/smith/budget/spent_reservation.rb +18 -0
- data/lib/smith/child_run.rb +41 -0
- data/lib/smith/child_runs_failed.rb +56 -0
- data/lib/smith/content.rb +96 -0
- data/lib/smith/context/session.rb +14 -2
- data/lib/smith/context/state_injection.rb +10 -5
- data/lib/smith/context.rb +28 -0
- data/lib/smith/doctor/checks/artifacts.rb +103 -0
- data/lib/smith/doctor/checks/configuration.rb +0 -11
- data/lib/smith/doctor/checks/durability.rb +20 -18
- data/lib/smith/doctor/checks/state_model.rb +63 -0
- data/lib/smith/doctor/installer.rb +9 -2
- data/lib/smith/doctor.rb +4 -0
- data/lib/smith/errors.rb +249 -2
- data/lib/smith/idempotency_key/canonical.rb +74 -0
- data/lib/smith/idempotency_key.rb +145 -0
- data/lib/smith/launchers/inline.rb +46 -0
- data/lib/smith/message.rb +76 -0
- data/lib/smith/models/tool_routing.rb +47 -44
- data/lib/smith/output_failure.rb +130 -0
- data/lib/smith/persistence_adapters/active_record_exact_store.rb +43 -0
- data/lib/smith/pricing/catalog_validation.rb +91 -0
- data/lib/smith/pricing.rb +95 -64
- data/lib/smith/providers/anthropic/batches.rb +116 -0
- data/lib/smith/providers/batch_exchange.rb +77 -0
- data/lib/smith/providers/openai/responses.rb +45 -20
- data/lib/smith/providers/output_limit.rb +68 -0
- data/lib/smith/providers/request_holds.rb +139 -0
- data/lib/smith/providers/response_facts.rb +108 -0
- data/lib/smith/providers/schema_constraint_check.rb +141 -0
- data/lib/smith/providers/schema_constraints.rb +87 -0
- data/lib/smith/providers/schema_rules.rb +66 -0
- data/lib/smith/providers/stop_reason.rb +39 -0
- data/lib/smith/providers/stream_facts.rb +192 -0
- data/lib/smith/response_memo.rb +152 -0
- data/lib/smith/setting_constructors.rb +47 -0
- data/lib/smith/streaming/json_field.rb +136 -0
- data/lib/smith/streaming/json_string.rb +91 -0
- data/lib/smith/streaming/session.rb +77 -0
- data/lib/smith/streaming/think_tags.rb +67 -0
- data/lib/smith/streaming.rb +64 -0
- data/lib/smith/testing/checks.rb +58 -0
- data/lib/smith/testing/hooks.rb +85 -0
- data/lib/smith/testing/minitest.rb +37 -0
- data/lib/smith/testing/rspec.rb +52 -0
- data/lib/smith/testing/stub.rb +110 -0
- data/lib/smith/testing.rb +158 -0
- data/lib/smith/tool/scoped_context.rb +9 -12
- data/lib/smith/tool.rb +14 -0
- data/lib/smith/trace/logger.rb +1 -1
- data/lib/smith/trace/memory.rb +1 -1
- data/lib/smith/trace/open_telemetry.rb +1 -1
- data/lib/smith/trace.rb +11 -4
- data/lib/smith/untrusted.rb +74 -0
- data/lib/smith/version.rb +2 -2
- data/lib/smith/workflow/active_clock.rb +61 -0
- data/lib/smith/workflow/agent_result.rb +37 -0
- data/lib/smith/workflow/artifact_integration.rb +57 -7
- data/lib/smith/workflow/await_step.rb +42 -0
- data/lib/smith/workflow/batch_charges.rb +74 -0
- data/lib/smith/workflow/batch_execution.rb +149 -0
- data/lib/smith/workflow/batch_holds.rb +73 -0
- data/lib/smith/workflow/batch_wait.rb +134 -0
- data/lib/smith/workflow/budget_integration.rb +11 -5
- data/lib/smith/workflow/budget_pause.rb +51 -0
- data/lib/smith/workflow/child_run_declarations.rb +127 -0
- data/lib/smith/workflow/child_run_execution.rb +150 -0
- data/lib/smith/workflow/child_run_results.rb +55 -0
- data/lib/smith/workflow/composite/effects.rb +37 -6
- data/lib/smith/workflow/composite/error.rb +1 -1
- data/lib/smith/workflow/composite/error_evidence.rb +9 -9
- data/lib/smith/workflow/composite/input.rb +11 -4
- data/lib/smith/workflow/context_keys.rb +77 -0
- data/lib/smith/workflow/deadline_enforcement.rb +1 -1
- data/lib/smith/workflow/deterministic_execution.rb +61 -10
- data/lib/smith/workflow/deterministic_step.rb +55 -6
- data/lib/smith/workflow/dsl.rb +59 -0
- data/lib/smith/workflow/durability.rb +139 -30
- data/lib/smith/workflow/evaluator_optimizer.rb +12 -6
- data/lib/smith/workflow/event_integration.rb +9 -0
- data/lib/smith/workflow/execution.rb +30 -23
- data/lib/smith/workflow/execution_binding_resolution.rb +13 -0
- data/lib/smith/workflow/failure_reconstructor.rb +27 -4
- data/lib/smith/workflow/failure_record.rb +6 -1
- data/lib/smith/workflow/failure_record_validator.rb +11 -4
- data/lib/smith/workflow/fanout_execution.rb +5 -8
- data/lib/smith/workflow/graph/batch_diagnostic.rb +83 -0
- data/lib/smith/workflow/graph/branch_limit_diagnostic.rb +41 -0
- data/lib/smith/workflow/graph/budget_ledger_diagnostic.rb +126 -0
- data/lib/smith/workflow/graph/durable_retry_diagnostic.rb +50 -0
- data/lib/smith/workflow/graph/execution_successors.rb +16 -2
- data/lib/smith/workflow/graph/inherited_untrusted_diagnostics.rb +77 -0
- data/lib/smith/workflow/graph/isolated_input_diagnostic.rb +50 -0
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +43 -4
- data/lib/smith/workflow/graph/output_key_diagnostic.rb +53 -0
- data/lib/smith/workflow/graph/output_schema_diagnostic.rb +49 -0
- data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +1 -0
- data/lib/smith/workflow/graph/run_launcher_diagnostic.rb +30 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb +9 -3
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +7 -3
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +8 -1
- data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +9 -2
- data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +7 -4
- data/lib/smith/workflow/graph/stream_diagnostic.rb +95 -0
- data/lib/smith/workflow/graph/transition_contract.rb +19 -0
- data/lib/smith/workflow/graph/transition_contract_attributes.rb +25 -2
- data/lib/smith/workflow/graph/transition_contract_configurations.rb +24 -3
- data/lib/smith/workflow/graph/transition_diagnostics.rb +11 -24
- data/lib/smith/workflow/graph/transition_snapshot.rb +40 -39
- data/lib/smith/workflow/graph/untrusted_input_diagnostic.rb +57 -0
- data/lib/smith/workflow/graph/waiting_child_diagnostic.rb +24 -0
- data/lib/smith/workflow/graph.rb +12 -0
- data/lib/smith/workflow/guarded_step_execution.rb +48 -3
- data/lib/smith/workflow/message_batch.rb +6 -0
- data/lib/smith/workflow/message_roles.rb +25 -0
- data/lib/smith/workflow/message_value_normalizer.rb +2 -1
- data/lib/smith/workflow/nested_execution.rb +47 -4
- data/lib/smith/workflow/orchestrator_worker.rb +9 -3
- data/lib/smith/workflow/parallel_execution.rb +10 -0
- data/lib/smith/workflow/persistence.rb +136 -13
- data/lib/smith/workflow/remembered_calls.rb +92 -0
- data/lib/smith/workflow/retry_execution.rb +68 -9
- data/lib/smith/workflow/retry_wait.rb +134 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint_state.rb +7 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +4 -1
- data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +3 -1
- data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +1 -0
- data/lib/smith/workflow/split_step_persistence/preparation.rb +5 -0
- data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +1 -0
- data/lib/smith/workflow/step_completion.rb +36 -7
- data/lib/smith/workflow/step_context.rb +21 -0
- data/lib/smith/workflow/step_io.rb +132 -0
- data/lib/smith/workflow/step_stream.rb +52 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +2 -0
- data/lib/smith/workflow/transition.rb +267 -13
- data/lib/smith/workflow/transition_actionability.rb +2 -0
- data/lib/smith/workflow/usage_entry.rb +24 -3
- data/lib/smith/workflow/wait_completion.rb +131 -0
- data/lib/smith/workflow/wait_execution.rb +180 -0
- data/lib/smith/workflow/wait_record.rb +157 -0
- data/lib/smith/workflow.rb +113 -17
- data/lib/smith.rb +101 -45
- metadata +88 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
# A map, call or spawn step needs a run launcher to start its runs.
|
|
9
|
+
class RunLauncherDiagnostic
|
|
10
|
+
extend Dry::Initializer
|
|
11
|
+
|
|
12
|
+
param :graph
|
|
13
|
+
|
|
14
|
+
def to_a
|
|
15
|
+
return [] if Smith.config.run_launcher
|
|
16
|
+
|
|
17
|
+
graph.reachable_transitions.select(&:child_runs?).map do |transition|
|
|
18
|
+
kind = transition.children_config.fetch(:kind)
|
|
19
|
+
Diagnostic.new(
|
|
20
|
+
severity: :error, code: :run_launcher_missing, transition: transition.name,
|
|
21
|
+
message: "Transition #{Reference.format(transition.name)} is a #{kind}, which starts runs through " \
|
|
22
|
+
"Smith.config.run_launcher, and none is configured.",
|
|
23
|
+
suggestion: "Set Smith.config.run_launcher (Smith::Launchers::Inline runs children in process)."
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -22,7 +22,7 @@ module Smith
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
agent_class = registry_binding.fetch(:agent_class)
|
|
25
|
-
return if agent_class.model_configured?
|
|
25
|
+
return if agent_class.model_configured? || fallback_models?(agent_class)
|
|
26
26
|
return required_model_diagnostic if model_required?
|
|
27
27
|
|
|
28
28
|
model_warning
|
|
@@ -80,18 +80,24 @@ module Smith
|
|
|
80
80
|
)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# An agent with neither a model nor fallback models fails when its
|
|
84
|
+
# step runs.
|
|
83
85
|
def model_warning
|
|
84
86
|
Diagnostic.new(
|
|
85
|
-
severity: :
|
|
87
|
+
severity: :error,
|
|
86
88
|
code: :agent_without_model,
|
|
87
89
|
transition: transition.name,
|
|
88
90
|
target: agent,
|
|
89
91
|
message: "Transition #{ref(transition.name)} references #{role} #{ref(agent)}, " \
|
|
90
92
|
"but the registered agent has no model configured.",
|
|
91
|
-
suggestion: "Configure a model for agent #{ref(agent)}
|
|
93
|
+
suggestion: "Configure a model (or fallback models) for agent #{ref(agent)}."
|
|
92
94
|
)
|
|
93
95
|
end
|
|
94
96
|
|
|
97
|
+
def fallback_models?(agent_class)
|
|
98
|
+
!agent_class.fallback_models_block.nil? || Array(agent_class.fallback_models).any?
|
|
99
|
+
end
|
|
100
|
+
|
|
95
101
|
def agent
|
|
96
102
|
binding.fetch(:agent)
|
|
97
103
|
end
|
|
@@ -11,9 +11,13 @@ module Smith
|
|
|
11
11
|
param :graph
|
|
12
12
|
|
|
13
13
|
def to_a
|
|
14
|
-
agent_bindings.
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
agent_bindings.flat_map do |binding|
|
|
15
|
+
[
|
|
16
|
+
RuntimeBindingDiagnosticBuilder.new(graph, binding: binding).to_diagnostic,
|
|
17
|
+
UntrustedInputDiagnostic.new(graph, binding:).call,
|
|
18
|
+
OutputSchemaDiagnostic.new(binding:).call
|
|
19
|
+
]
|
|
20
|
+
end.compact
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def agent_bindings
|
|
@@ -31,10 +31,17 @@ module Smith
|
|
|
31
31
|
invalid_agent_bindings_count: diagnostic_count(:invalid_agent_binding),
|
|
32
32
|
uninspectable_agent_bindings_count: diagnostic_count(:uninspectable_agent_binding),
|
|
33
33
|
modelless_agent_bindings_count: diagnostic_count(:agent_without_model),
|
|
34
|
-
required_model_missing_count: diagnostic_count(:agent_without_required_model)
|
|
34
|
+
required_model_missing_count: diagnostic_count(:agent_without_required_model),
|
|
35
|
+
budget_ledger: budget_ledger_metric
|
|
35
36
|
}
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# The scope keys of the workflow's budget_ledger, or nil for none.
|
|
40
|
+
def budget_ledger_metric
|
|
41
|
+
klass = graph.workflow_class
|
|
42
|
+
{ scope_keys: klass.budget_ledger_scope_keys } if BudgetLedgerDiagnostic.declared?(klass)
|
|
43
|
+
end
|
|
44
|
+
|
|
38
45
|
def binding_metrics
|
|
39
46
|
{
|
|
40
47
|
direct_agent_bindings_count: agent_bindings.length,
|
|
@@ -11,12 +11,13 @@ module Smith
|
|
|
11
11
|
param :graph
|
|
12
12
|
option :reports
|
|
13
13
|
option :cycle_transitions
|
|
14
|
+
option :graphs, default: proc { {}.compare_by_identity }
|
|
14
15
|
|
|
15
16
|
def call
|
|
16
17
|
topology_report = graph.validate
|
|
17
18
|
bindings = RuntimeBindingDiagnostics.new(graph)
|
|
18
|
-
nested = NestedReadinessDiagnostics.new(graph, reports:, cycle_transitions:)
|
|
19
|
-
diagnostics =
|
|
19
|
+
nested = NestedReadinessDiagnostics.new(graph, reports:, cycle_transitions:, graphs:)
|
|
20
|
+
diagnostics = runtime_diagnostics(bindings, nested)
|
|
20
21
|
RuntimeReadinessReport.new(
|
|
21
22
|
workflow_class: workflow_label,
|
|
22
23
|
topology_report: topology_report,
|
|
@@ -27,6 +28,12 @@ module Smith
|
|
|
27
28
|
|
|
28
29
|
private
|
|
29
30
|
|
|
31
|
+
def runtime_diagnostics(bindings, nested)
|
|
32
|
+
[*bindings.to_a, *RunLauncherDiagnostic.new(graph).to_a, *DurableRetryDiagnostic.new(graph).to_a,
|
|
33
|
+
*StreamDiagnostic.new(graph).to_a, *BatchDiagnostic.new(graph).to_a,
|
|
34
|
+
*BudgetLedgerDiagnostic.new(graph).to_a, *nested.to_a]
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
def metrics(topology_report, diagnostics, bindings, nested)
|
|
31
38
|
RuntimeReadinessMetrics.new(
|
|
32
39
|
graph,
|
|
@@ -62,9 +62,9 @@ module Smith
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def visit_child(parent_graph, transition, frames)
|
|
65
|
-
workflow_class = transition.
|
|
65
|
+
workflow_class = transition.target_workflow_class
|
|
66
66
|
case @states[workflow_class]
|
|
67
|
-
when :visiting then mark_cycle(parent_graph, transition)
|
|
67
|
+
when :visiting then mark_cycle(parent_graph, transition) unless transition.child_runs?
|
|
68
68
|
when :done then nil
|
|
69
69
|
else
|
|
70
70
|
child_graph = graph_for(workflow_class)
|
|
@@ -78,7 +78,8 @@ module Smith
|
|
|
78
78
|
@reports[workflow_class] = RuntimeReadinessReportBuilder.new(
|
|
79
79
|
current_graph,
|
|
80
80
|
reports: @reports,
|
|
81
|
-
cycle_transitions: cycles_for(current_graph)
|
|
81
|
+
cycle_transitions: cycles_for(current_graph),
|
|
82
|
+
graphs: @graphs
|
|
82
83
|
).call
|
|
83
84
|
@states[workflow_class] = :done
|
|
84
85
|
end
|
|
@@ -88,7 +89,9 @@ module Smith
|
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
def nested_transitions_for(current_graph)
|
|
91
|
-
@nested_transitions[current_graph] ||= current_graph.reachable_transitions.select
|
|
92
|
+
@nested_transitions[current_graph] ||= current_graph.reachable_transitions.select do |transition|
|
|
93
|
+
transition.nested? || transition.child_runs?
|
|
94
|
+
end.freeze
|
|
92
95
|
end
|
|
93
96
|
|
|
94
97
|
def mark_cycle(current_graph, transition)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
# A streamed step needs its sink in Smith.config.stream_sinks (the
|
|
9
|
+
# step fails without it). An agent whose calls go to OpenAI's Responses
|
|
10
|
+
# API (asked for in its params, or chosen by Smith's tool routing under
|
|
11
|
+
# `openai_api_mode` :auto, as at runtime) is not streamed: its sink
|
|
12
|
+
# gets each reply as one delta.
|
|
13
|
+
class StreamDiagnostic
|
|
14
|
+
extend Dry::Initializer
|
|
15
|
+
|
|
16
|
+
param :graph
|
|
17
|
+
|
|
18
|
+
def to_a
|
|
19
|
+
graph.reachable_transitions.filter_map do |transition|
|
|
20
|
+
next unless transition.stream
|
|
21
|
+
next missing_sink(transition) unless Smith.config.stream_sinks.key?(transition.stream)
|
|
22
|
+
|
|
23
|
+
whole_replies(transition) if responses_route?(transition.agent_name)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def responses_route?(agent)
|
|
30
|
+
agent_class = Agent::Registry.binding_for(agent)&.fetch(:agent_class)
|
|
31
|
+
return false unless agent_class.respond_to?(:chat_kwargs)
|
|
32
|
+
|
|
33
|
+
profiles = static_models(agent_class).filter_map { profile(_1) }.select { _1.provider == :openai }
|
|
34
|
+
return false if profiles.empty?
|
|
35
|
+
return true if params(agent_class)[:openai_api_mode].to_s == "responses"
|
|
36
|
+
|
|
37
|
+
profiles.any? { |profile| routed?(profile, agent_class) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Tools given by a block are known only per run: they may route.
|
|
41
|
+
def routed?(profile, agent_class)
|
|
42
|
+
tools = agent_class.respond_to?(:tools) ? agent_class.tools : []
|
|
43
|
+
return Smith.config.openai_api_mode == :auto unless tools.is_a?(Array)
|
|
44
|
+
|
|
45
|
+
Smith::Models::ToolRouting.responses?(profile:, tools:, thinking: thinking?(agent_class))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def static_models(agent_class)
|
|
49
|
+
primary = agent_class.chat_kwargs[:model] &&
|
|
50
|
+
Agent::ModelReference.new(model_id: agent_class.chat_kwargs[:model],
|
|
51
|
+
provider: agent_class.chat_kwargs[:provider])
|
|
52
|
+
fallbacks = agent_class.fallback_models_block ? [] : Array(agent_class.fallback_models)
|
|
53
|
+
[primary, *fallbacks.map { Agent::ModelReference.coerce(_1) }].compact
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def profile(reference)
|
|
57
|
+
Smith::Models.find_or_infer(reference.model_id, provider: reference.provider&.to_sym)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def params(agent_class)
|
|
63
|
+
params = agent_class.params if agent_class.respond_to?(:params)
|
|
64
|
+
params.is_a?(Hash) ? params.transform_keys(&:to_sym) : {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def thinking?(agent_class)
|
|
68
|
+
configured = agent_class.thinking if agent_class.respond_to?(:thinking)
|
|
69
|
+
!configured.nil? || params(agent_class).keys.intersect?(%i[thinking reasoning reasoning_effort])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def missing_sink(transition)
|
|
73
|
+
Diagnostic.new(
|
|
74
|
+
severity: :error, code: :stream_sink_missing, transition: transition.name, target: transition.stream,
|
|
75
|
+
message: "Transition #{Reference.format(transition.name)} streams to #{transition.stream.inspect}, " \
|
|
76
|
+
"which Smith.config.stream_sinks does not name.",
|
|
77
|
+
suggestion: "Register the sink: Smith.config.stream_sinks = { #{transition.stream.inspect} => sink }."
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def whole_replies(transition)
|
|
82
|
+
Diagnostic.new(
|
|
83
|
+
severity: :warning, code: :stream_whole_replies, transition: transition.name,
|
|
84
|
+
target: transition.agent_name,
|
|
85
|
+
message: "Transition #{Reference.format(transition.name)} streams agent " \
|
|
86
|
+
"#{Reference.format(transition.agent_name)}, whose calls go to OpenAI's Responses API, which " \
|
|
87
|
+
"does not stream: its sink gets each reply as one delta.",
|
|
88
|
+
suggestion: "Accept whole replies there, or keep the agent on Chat Completions " \
|
|
89
|
+
"(Smith.config.openai_api_mode = :off, and no openai_api_mode param)."
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -23,8 +23,17 @@ module Smith
|
|
|
23
23
|
option :orchestrator_config, default: proc {}
|
|
24
24
|
option :fanout_config, default: proc {}
|
|
25
25
|
option :retry_config, default: proc {}
|
|
26
|
+
option :await_config, default: proc {}
|
|
27
|
+
option :timeout_transition, default: proc {}
|
|
28
|
+
option :step_input, default: proc { false }
|
|
29
|
+
option :output_to, default: proc {}
|
|
30
|
+
option :stream, default: proc {}
|
|
31
|
+
option :batch_mode, default: proc {}
|
|
32
|
+
option :on_request_change, default: proc {}
|
|
33
|
+
option :children_config, default: proc {}
|
|
26
34
|
option :deterministic_kind, default: proc {}
|
|
27
35
|
option :deterministic_routes, default: proc {}
|
|
36
|
+
option :deterministic_fallthrough, default: proc { false }
|
|
28
37
|
option :deterministic, default: proc { false }
|
|
29
38
|
option :parallel, default: proc { false }
|
|
30
39
|
option :parallel_count, default: proc {}
|
|
@@ -41,6 +50,16 @@ module Smith
|
|
|
41
50
|
def nested? = !workflow_class.nil?
|
|
42
51
|
def routed? = !router_config.nil?
|
|
43
52
|
def parallel? = parallel
|
|
53
|
+
def awaits? = !await_config.nil?
|
|
54
|
+
def delay? = await_config&.fetch(:kind) == :delay
|
|
55
|
+
def agent_step? = !agent_name.nil? || optimized? || orchestrated? || fanout?
|
|
56
|
+
def child_runs? = !children_config.nil?
|
|
57
|
+
def spawns? = children_config&.fetch(:kind) == :spawn
|
|
58
|
+
def batches? = !batch_mode.nil? && batch_mode != :sync
|
|
59
|
+
|
|
60
|
+
# The workflow class this step runs: nested in process, or launched
|
|
61
|
+
# as runs of their own.
|
|
62
|
+
def target_workflow_class = workflow_class || children_config&.fetch(:workflow)
|
|
44
63
|
end
|
|
45
64
|
end
|
|
46
65
|
end
|
|
@@ -36,7 +36,9 @@ module Smith
|
|
|
36
36
|
{
|
|
37
37
|
success_transition: project(transition.success_transition),
|
|
38
38
|
failure_transition: project(transition.failure_transition),
|
|
39
|
-
|
|
39
|
+
timeout_transition: project(transition.timeout_transition),
|
|
40
|
+
deterministic_routes: project_array(transition.deterministic_routes),
|
|
41
|
+
deterministic_fallthrough: transition.deterministic_fallthrough == true
|
|
40
42
|
}
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -45,10 +47,31 @@ module Smith
|
|
|
45
47
|
deterministic_kind: transition.deterministic_kind,
|
|
46
48
|
deterministic: transition.deterministic?,
|
|
47
49
|
parallel: transition.parallel?,
|
|
48
|
-
parallel_count: static_parallel_count
|
|
50
|
+
parallel_count: static_parallel_count,
|
|
51
|
+
step_input: !transition.step_input.nil?,
|
|
52
|
+
output_to: transition.output_to,
|
|
53
|
+
**call_attributes,
|
|
54
|
+
children_config: children_config
|
|
49
55
|
}
|
|
50
56
|
end
|
|
51
57
|
|
|
58
|
+
# How the step's calls are made: streamed to a sink, and :sync,
|
|
59
|
+
# :batch or :callable (a mode chosen per attempt), and what a batch
|
|
60
|
+
# result does when the step renders another request by then.
|
|
61
|
+
def call_attributes
|
|
62
|
+
mode = transition.batch_mode
|
|
63
|
+
{ stream: transition.stream, batch_mode: mode.respond_to?(:call) ? :callable : mode,
|
|
64
|
+
on_request_change: transition.on_request_change }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def children_config
|
|
68
|
+
config = transition.children_config
|
|
69
|
+
return unless config
|
|
70
|
+
|
|
71
|
+
items = config[:items].respond_to?(:call) ? :callable : config[:items]
|
|
72
|
+
config.slice(:kind, :workflow, :max_items, :collect, :on_item_failure).merge(items:).freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
52
75
|
def static_parallel_count
|
|
53
76
|
return unless transition.parallel?
|
|
54
77
|
|
|
@@ -19,7 +19,8 @@ module Smith
|
|
|
19
19
|
optimization_config: optimization_config,
|
|
20
20
|
orchestrator_config: orchestrator_config,
|
|
21
21
|
fanout_config: fanout_config,
|
|
22
|
-
retry_config: retry_config
|
|
22
|
+
retry_config: retry_config,
|
|
23
|
+
await_config: await_config
|
|
23
24
|
}
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -61,17 +62,37 @@ module Smith
|
|
|
61
62
|
{ branches: own_hash(branches) }.freeze if branches
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
# The wait's kind, event, context key and timeout (`:callable` when
|
|
66
|
+
# the timeout is computed from the context); the block is not shown.
|
|
67
|
+
def await_config
|
|
68
|
+
config = transition.await_config
|
|
69
|
+
return unless config
|
|
70
|
+
|
|
71
|
+
timeout = config.fetch(:timeout)
|
|
72
|
+
{
|
|
73
|
+
kind: config.fetch(:kind), event: own(config[:event]), into: config[:into],
|
|
74
|
+
timeout: timeout.respond_to?(:call) ? :callable : timeout
|
|
75
|
+
}.compact.freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
64
78
|
def retry_config
|
|
65
79
|
config = transition.retry_config
|
|
66
80
|
return unless config
|
|
67
81
|
|
|
68
|
-
{
|
|
82
|
+
exported = {
|
|
69
83
|
error_classes: config.fetch(:error_classes).dup.freeze,
|
|
70
84
|
attempts: config.fetch(:attempts),
|
|
71
85
|
backoff: config.fetch(:backoff),
|
|
72
86
|
max_delay: config[:max_delay],
|
|
73
87
|
jitter: config.fetch(:jitter)
|
|
74
|
-
}
|
|
88
|
+
}
|
|
89
|
+
exported.merge(optional_retry_fields(config)).freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def optional_retry_fields(config)
|
|
93
|
+
new_key_classes = config.fetch(:new_idempotency_key_on, [])
|
|
94
|
+
{ new_idempotency_key_on: (new_key_classes.dup.freeze if new_key_classes.any?),
|
|
95
|
+
durable: (true if config[:durable]) }.compact
|
|
75
96
|
end
|
|
76
97
|
|
|
77
98
|
def own_hash(hash)
|
|
@@ -4,6 +4,10 @@ module Smith
|
|
|
4
4
|
class Workflow
|
|
5
5
|
class Graph
|
|
6
6
|
class TransitionDiagnostics
|
|
7
|
+
TARGET_ATTRIBUTES = %i[success_transition failure_transition timeout_transition].freeze
|
|
8
|
+
DECLARATION_CHECKS = [
|
|
9
|
+
BranchLimitDiagnostic, RetryPolicyDiagnostic, IsolatedInputDiagnostic, OutputKeyDiagnostic
|
|
10
|
+
].freeze
|
|
7
11
|
attr_reader :graph
|
|
8
12
|
|
|
9
13
|
def initialize(graph)
|
|
@@ -13,13 +17,11 @@ module Smith
|
|
|
13
17
|
def to_a
|
|
14
18
|
graph.transitions.values.flat_map do |transition|
|
|
15
19
|
[
|
|
16
|
-
transition_target_diagnostic(transition,
|
|
17
|
-
transition_target_diagnostic(transition, :failure_transition, transition.failure_transition),
|
|
20
|
+
*TARGET_ATTRIBUTES.map { transition_target_diagnostic(transition, _1, transition.public_send(_1)) },
|
|
18
21
|
*router_target_diagnostics(transition),
|
|
19
22
|
*deterministic_route_diagnostics(transition),
|
|
20
23
|
*target_state_mismatch_diagnostics(transition),
|
|
21
|
-
|
|
22
|
-
RetryPolicyDiagnostic.new(transition:).call
|
|
24
|
+
*declaration_diagnostics(transition)
|
|
23
25
|
].compact
|
|
24
26
|
end
|
|
25
27
|
end
|
|
@@ -63,29 +65,14 @@ module Smith
|
|
|
63
65
|
transition.failure_transition,
|
|
64
66
|
expected_state: transition.from
|
|
65
67
|
)
|
|
68
|
+
timeout = target_state_mismatch_diagnostic(transition, transition.timeout_transition,
|
|
69
|
+
expected_state: transition.from)
|
|
66
70
|
|
|
67
|
-
[*success, failure].compact
|
|
71
|
+
[*success, failure, timeout].compact
|
|
68
72
|
end
|
|
69
73
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
limit = Smith.config.parallel_branch_limit
|
|
73
|
-
return unless branch_count && branch_count > limit
|
|
74
|
-
|
|
75
|
-
Diagnostic.new(
|
|
76
|
-
severity: :error,
|
|
77
|
-
code: :parallel_branch_limit_exceeded,
|
|
78
|
-
transition: transition.name,
|
|
79
|
-
message: "Transition #{ref(transition.name)} declares #{branch_count} parallel branches, " \
|
|
80
|
-
"exceeding the configured limit #{limit}.",
|
|
81
|
-
suggestion: "Reduce the branch count or explicitly raise Smith.config.parallel_branch_limit."
|
|
82
|
-
)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def static_branch_count(transition)
|
|
86
|
-
return transition.fanout_config.fetch(:branches).length if transition.fanout?
|
|
87
|
-
|
|
88
|
-
transition.parallel_count if transition.parallel?
|
|
74
|
+
def declaration_diagnostics(transition)
|
|
75
|
+
DECLARATION_CHECKS.map { _1.new(transition:, workflow_class: graph.workflow_class).call }
|
|
89
76
|
end
|
|
90
77
|
|
|
91
78
|
def successful_target_names(transition)
|
|
@@ -29,14 +29,23 @@ module Smith
|
|
|
29
29
|
option :parallel_count, default: proc {}
|
|
30
30
|
option :success_transition, default: proc {}
|
|
31
31
|
option :failure_transition, default: proc {}
|
|
32
|
+
option :timeout_transition, default: proc {}
|
|
32
33
|
option :routes, default: proc {}
|
|
33
34
|
option :fallback, default: proc {}
|
|
34
35
|
option :deterministic_routes, default: proc {}
|
|
36
|
+
option :deterministic_fallthrough, default: proc {}
|
|
35
37
|
option :fanout_branches, default: proc {}
|
|
36
38
|
option :fanout, default: proc {}
|
|
37
39
|
option :optimization, default: proc {}
|
|
38
40
|
option :orchestration, default: proc {}
|
|
39
41
|
option :retry_policy, default: proc {}
|
|
42
|
+
option :wait, default: proc {}
|
|
43
|
+
option :input, default: proc {}
|
|
44
|
+
option :output_to, default: proc {}
|
|
45
|
+
option :stream, default: proc {}
|
|
46
|
+
option :mode, default: proc {}
|
|
47
|
+
option :on_request_change, default: proc {}
|
|
48
|
+
option :children, default: proc {}
|
|
40
49
|
|
|
41
50
|
def self.from_transition(transition, workflow_class: nil)
|
|
42
51
|
new(**attributes_for(transition, workflow_class: workflow_class))
|
|
@@ -56,48 +65,56 @@ module Smith
|
|
|
56
65
|
{
|
|
57
66
|
success_transition: transition.success_transition,
|
|
58
67
|
failure_transition: transition.failure_transition,
|
|
68
|
+
timeout_transition: transition.timeout_transition,
|
|
59
69
|
routes: transition.router_config&.fetch(:routes, nil),
|
|
60
70
|
fallback: transition.router_config&.fetch(:fallback, nil),
|
|
61
71
|
deterministic_routes: transition.deterministic_routes,
|
|
72
|
+
deterministic_fallthrough: transition.deterministic_routes && transition.deterministic_fallthrough == true,
|
|
62
73
|
fanout_branches: transition.fanout_config&.fetch(:branches, nil)
|
|
63
74
|
}
|
|
64
75
|
end
|
|
65
76
|
|
|
66
77
|
def self.contract_attributes(transition, workflow_class:)
|
|
67
78
|
{
|
|
68
|
-
fanout:
|
|
69
|
-
optimization:
|
|
70
|
-
orchestration:
|
|
71
|
-
retry_policy: retry_policy_for(transition)
|
|
79
|
+
fanout: FanoutContract.from_transition(transition, workflow_class:),
|
|
80
|
+
optimization: OptimizationContract.from_transition(transition, workflow_class:),
|
|
81
|
+
orchestration: OrchestrationContract.from_transition(transition, workflow_class:),
|
|
82
|
+
retry_policy: retry_policy_for(transition),
|
|
83
|
+
wait: transition.await_config,
|
|
84
|
+
input: (:callable if transition.step_input),
|
|
85
|
+
output_to: transition.output_to,
|
|
86
|
+
stream: transition.stream,
|
|
87
|
+
mode: transition.batch_mode,
|
|
88
|
+
on_request_change: transition.on_request_change,
|
|
89
|
+
children: children_for(transition)
|
|
72
90
|
}
|
|
73
91
|
end
|
|
74
92
|
|
|
75
|
-
def self.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def self.optimization_for(transition, workflow_class:)
|
|
80
|
-
OptimizationContract.from_transition(transition, workflow_class: workflow_class)
|
|
81
|
-
end
|
|
93
|
+
def self.retry_policy_for(transition)
|
|
94
|
+
config = transition.retry_config
|
|
95
|
+
return unless config
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
new_key_classes = config.fetch(:new_idempotency_key_on, [])
|
|
98
|
+
{ attempts: config.fetch(:attempts), error_classes: config.fetch(:error_classes).map(&:name),
|
|
99
|
+
backoff: config.fetch(:backoff), max_delay: config[:max_delay], jitter: config.fetch(:jitter),
|
|
100
|
+
new_idempotency_key_on: (new_key_classes.map(&:name) if new_key_classes.any?),
|
|
101
|
+
durable: (true if config[:durable]) }.compact
|
|
85
102
|
end
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
# A map, call or spawn: its child workflow (by name), bound, collect
|
|
105
|
+
# and failure modes, and its items (a context key, or :callable).
|
|
106
|
+
def self.children_for(transition)
|
|
107
|
+
config = transition.children_config
|
|
89
108
|
return unless config
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
error_classes: config.fetch(:error_classes).map(&:name),
|
|
94
|
-
backoff: config.fetch(:backoff),
|
|
95
|
-
max_delay: config[:max_delay],
|
|
96
|
-
jitter: config.fetch(:jitter)
|
|
97
|
-
}.compact
|
|
110
|
+
workflow = config[:workflow]
|
|
111
|
+
config.merge(workflow: workflow.name || workflow.inspect)
|
|
98
112
|
end
|
|
99
113
|
|
|
100
114
|
def self.kind_for(transition)
|
|
115
|
+
return transition.children_config.fetch(:kind) if transition.child_runs?
|
|
116
|
+
return transition.await_config.fetch(:kind) if transition.awaits?
|
|
117
|
+
|
|
101
118
|
kind = KINDS.find { |_name, predicate| transition.public_send(predicate) }
|
|
102
119
|
return kind.first if kind
|
|
103
120
|
return :agent if transition.agent_name
|
|
@@ -106,23 +123,7 @@ module Smith
|
|
|
106
123
|
end
|
|
107
124
|
|
|
108
125
|
def to_h
|
|
109
|
-
|
|
110
|
-
name: name,
|
|
111
|
-
from: from,
|
|
112
|
-
to: to,
|
|
113
|
-
kind: kind,
|
|
114
|
-
parallel_count: parallel_count,
|
|
115
|
-
success_transition: success_transition,
|
|
116
|
-
failure_transition: failure_transition,
|
|
117
|
-
routes: routes,
|
|
118
|
-
fallback: fallback,
|
|
119
|
-
deterministic_routes: deterministic_routes,
|
|
120
|
-
fanout_branches: fanout_branches,
|
|
121
|
-
fanout: fanout,
|
|
122
|
-
optimization: optimization,
|
|
123
|
-
orchestration: orchestration,
|
|
124
|
-
retry_policy: retry_policy
|
|
125
|
-
}.compact
|
|
126
|
+
self.class.dry_initializer.attributes(self).compact
|
|
126
127
|
end
|
|
127
128
|
end
|
|
128
129
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
class Graph
|
|
8
|
+
# An agent that names a context key the workflow's context marks
|
|
9
|
+
# untrusted among its `inputs` is refused at invocation.
|
|
10
|
+
class UntrustedInputDiagnostic
|
|
11
|
+
extend Dry::Initializer
|
|
12
|
+
|
|
13
|
+
param :graph
|
|
14
|
+
option :binding
|
|
15
|
+
|
|
16
|
+
# The context keys an agent reads as inputs: its declared `inputs`,
|
|
17
|
+
# without the reserved names Smith never reads from the context.
|
|
18
|
+
def self.context_inputs(agent_name)
|
|
19
|
+
agent_class = Agent::Registry.binding_for(agent_name)&.fetch(:agent_class)
|
|
20
|
+
inputs = agent_class.respond_to?(:inputs) ? Array(agent_class.inputs) : []
|
|
21
|
+
inputs - Agent::RESERVED_INPUT_NAMES
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The untrusted context keys a workflow class declares.
|
|
25
|
+
def self.declared_keys(workflow_class)
|
|
26
|
+
manager = workflow_class.context_manager if workflow_class.respond_to?(:context_manager)
|
|
27
|
+
manager.respond_to?(:untrusted_keys) ? manager.untrusted_keys : []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.suggestion(key)
|
|
31
|
+
"Pass #{Reference.format(key)} through the step's input:, wrapped with Smith::Untrusted.wrap."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call
|
|
35
|
+
key = untrusted_input
|
|
36
|
+
return unless key
|
|
37
|
+
|
|
38
|
+
Diagnostic.new(
|
|
39
|
+
severity: :error, code: :untrusted_agent_input, transition: binding.fetch(:transition).name,
|
|
40
|
+
target: binding.fetch(:agent), message: message(key), suggestion: self.class.suggestion(key)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def untrusted_input
|
|
45
|
+
(self.class.context_inputs(binding.fetch(:agent)) & self.class.declared_keys(graph.workflow_class)).first
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def message(key)
|
|
49
|
+
role = binding.fetch(:role)
|
|
50
|
+
"#{role[0].upcase}#{role[1..]} #{Reference.format(binding.fetch(:agent))} on transition " \
|
|
51
|
+
"#{Reference.format(binding.fetch(:transition).name)} declares untrusted context key " \
|
|
52
|
+
"#{Reference.format(key)} as an input."
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
# An in-process child (a nested workflow, or a child run the launcher
|
|
7
|
+
# runs in process) that reaches a step that waits, or a nested
|
|
8
|
+
# workflow that launches runs of its own.
|
|
9
|
+
module WaitingChildDiagnostic
|
|
10
|
+
def self.build(transition, label)
|
|
11
|
+
runs = transition.child_runs?
|
|
12
|
+
verb = runs ? "starts in-process runs of" : "nests"
|
|
13
|
+
Diagnostic.new(
|
|
14
|
+
severity: :error, code: :nested_workflow_waits, transition: transition.name, target: label,
|
|
15
|
+
message: "Transition #{Reference.format(transition.name)} #{verb} #{label}, " \
|
|
16
|
+
"which reaches a step that waits#{" or launches runs of its own" unless runs}.",
|
|
17
|
+
suggestion: "An in-process child cannot wait#{" or launch" unless runs}; move the step into the parent " \
|
|
18
|
+
"workflow#{", or use a launcher that queues children" if runs}."
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|