smith-agents 0.10.0 → 0.12.0
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 +901 -0
- data/README.md +636 -20
- data/UPSTREAM_PROPOSAL.md +1 -0
- data/docs/CONFIGURATION.md +100 -14
- data/docs/PATTERNS.md +88 -12
- data/docs/PERSISTENCE.md +738 -6
- data/docs/TOOLS_AND_GUARDRAILS.md +24 -3
- data/lib/generators/smith/install/install_generator.rb +63 -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/fallback_configuration.rb +26 -3
- 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 +17 -4
- data/lib/smith/agent/provider_call_timing.rb +71 -7
- data/lib/smith/agent/provider_completion.rb +41 -38
- 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 +75 -2
- 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 -48
- data/lib/smith/agent.rb +14 -1
- 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/amount_representation.rb +20 -0
- data/lib/smith/budget/continuation.rb +153 -0
- data/lib/smith/budget/host_ledger.rb +137 -0
- data/lib/smith/budget/ledger.rb +10 -4
- 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 +21 -6
- data/lib/smith/context.rb +28 -0
- data/lib/smith/diagnostic_text.rb +10 -1
- 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 +35 -17
- data/lib/smith/doctor/checks/persistence_capabilities.rb +13 -7
- 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 +304 -6
- 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 +51 -3
- data/lib/smith/persistence_adapters/active_record_store.rb +4 -7
- data/lib/smith/persistence_adapters.rb +10 -0
- data/lib/smith/pricing/catalog_validation.rb +91 -0
- data/lib/smith/pricing.rb +95 -64
- data/lib/smith/provider_permanent_failure.rb +44 -0
- 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 +14 -8
- 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 +62 -11
- data/lib/smith/workflow/deterministic_step.rb +56 -7
- data/lib/smith/workflow/dsl.rb +74 -5
- data/lib/smith/workflow/durability.rb +140 -31
- data/lib/smith/workflow/evaluator_optimizer.rb +116 -12
- data/lib/smith/workflow/event_integration.rb +9 -0
- data/lib/smith/workflow/execution.rb +32 -23
- data/lib/smith/workflow/execution_binding_resolution.rb +13 -0
- data/lib/smith/workflow/failure_reconstructor.rb +41 -2
- data/lib/smith/workflow/failure_record.rb +12 -12
- data/lib/smith/workflow/failure_record_validator.rb +32 -5
- 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/guardrail_integration.rb +2 -0
- 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/optimization_state.rb +2 -2
- data/lib/smith/workflow/orchestrator_worker.rb +9 -3
- data/lib/smith/workflow/parallel_execution.rb +10 -0
- data/lib/smith/workflow/persistence.rb +162 -22
- data/lib/smith/workflow/remembered_calls.rb +92 -0
- data/lib/smith/workflow/retry_execution.rb +71 -8
- data/lib/smith/workflow/retry_wait.rb +134 -0
- data/lib/smith/workflow/run_result.rb +10 -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 +40 -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 +117 -19
- data/lib/smith.rb +103 -47
- metadata +88 -1
|
@@ -6,19 +6,34 @@ module Smith
|
|
|
6
6
|
MARKER = "[smith:injected-state]"
|
|
7
7
|
|
|
8
8
|
def self.inject(messages, formatter:, persisted:)
|
|
9
|
-
|
|
9
|
+
text = formatter.call(persisted).to_s
|
|
10
|
+
content = "#{MARKER}\n#{text}"
|
|
10
11
|
|
|
11
|
-
existing_index = messages.index
|
|
12
|
-
message_content = message[:content] || message["content"]
|
|
13
|
-
message_content.is_a?(String) && message_content.start_with?(MARKER)
|
|
14
|
-
end
|
|
12
|
+
existing_index = messages.index { |message| injected_state?(message) }
|
|
15
13
|
|
|
16
|
-
if
|
|
14
|
+
if blank?(text)
|
|
15
|
+
without_marker(messages, existing_index)
|
|
16
|
+
elsif existing_index
|
|
17
17
|
messages.dup.tap { |msgs| msgs[existing_index] = { role: :system, content: content } }
|
|
18
18
|
else
|
|
19
19
|
messages + [{ role: :system, content: content }]
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Only a system message carries the marker: hosts cannot admit system
|
|
24
|
+
# messages, so a user, assistant or tool message that starts with the
|
|
25
|
+
# marker is content, never Smith's injected state.
|
|
26
|
+
def self.injected_state?(message)
|
|
27
|
+
content = message[:content] || message["content"]
|
|
28
|
+
(message[:role] || message["role"]).to_s == "system" && content.is_a?(String) && content.start_with?(MARKER)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.blank?(text) = text.valid_encoding? && text.strip.empty?
|
|
32
|
+
|
|
33
|
+
def self.without_marker(messages, index)
|
|
34
|
+
index ? messages.dup.tap { |msgs| msgs.delete_at(index) } : messages
|
|
35
|
+
end
|
|
36
|
+
private_class_method :blank?, :without_marker, :injected_state?
|
|
22
37
|
end
|
|
23
38
|
end
|
|
24
39
|
end
|
data/lib/smith/context.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Smith
|
|
|
10
10
|
subclass.instance_variable_set(:@persist_mode, @persist_mode || :explicit)
|
|
11
11
|
subclass.instance_variable_set(:@persist_auto_seed, (@persist_auto_seed || []).dup)
|
|
12
12
|
subclass.instance_variable_set(:@inject_state, @inject_state)
|
|
13
|
+
subclass.instance_variable_set(:@untrusted_keys, (@untrusted_keys || []).dup)
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def session_strategy(strategy = nil, **opts)
|
|
@@ -23,6 +24,7 @@ module Smith
|
|
|
23
24
|
return @persist_keys || []
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
validate_persist_keys!(keys)
|
|
26
28
|
if also && !keys.include?(:auto)
|
|
27
29
|
raise Smith::WorkflowError, ":also is only valid alongside :auto"
|
|
28
30
|
end
|
|
@@ -47,10 +49,36 @@ module Smith
|
|
|
47
49
|
@persist_mode || :explicit
|
|
48
50
|
end
|
|
49
51
|
|
|
52
|
+
def validate_persist_keys!(keys)
|
|
53
|
+
invalid = keys.grep_v(Symbol)
|
|
54
|
+
return if invalid.empty?
|
|
55
|
+
|
|
56
|
+
raise Smith::WorkflowError, "persist keys must be Symbols, got #{invalid.map(&:inspect).join(", ")}"
|
|
57
|
+
end
|
|
58
|
+
private :validate_persist_keys!
|
|
59
|
+
|
|
50
60
|
def persist_auto_seed
|
|
51
61
|
@persist_auto_seed || []
|
|
52
62
|
end
|
|
53
63
|
|
|
64
|
+
# Keys whose values come from outside the system (a fetched document,
|
|
65
|
+
# an inbound message), or that a step derived from such a value:
|
|
66
|
+
# `inject_state` formatters and the `seed_messages` builder never
|
|
67
|
+
# receive them, and an agent that names one in `inputs` is refused,
|
|
68
|
+
# in this workflow and in any workflow it nests.
|
|
69
|
+
def untrusted(*keys)
|
|
70
|
+
return untrusted_keys if keys.empty?
|
|
71
|
+
|
|
72
|
+
validate_persist_keys!(keys)
|
|
73
|
+
raise Smith::WorkflowError, "untrusted takes context keys, not :auto" if keys.include?(:auto)
|
|
74
|
+
|
|
75
|
+
@untrusted_keys = (untrusted_keys + keys).uniq
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def untrusted_keys
|
|
79
|
+
@untrusted_keys || []
|
|
80
|
+
end
|
|
81
|
+
|
|
54
82
|
def inject_state(&block)
|
|
55
83
|
return @inject_state unless block_given?
|
|
56
84
|
|
|
@@ -9,7 +9,10 @@ module Smith
|
|
|
9
9
|
STRING_BYTESIZE = String.instance_method(:bytesize)
|
|
10
10
|
STRING_BYTESLICE = String.instance_method(:byteslice)
|
|
11
11
|
MODULE_MATCH = Module.instance_method(:===)
|
|
12
|
-
|
|
12
|
+
MODULE_NAME = Module.instance_method(:name)
|
|
13
|
+
OBJECT_CLASS = Object.instance_method(:class)
|
|
14
|
+
private_constant :TRUNCATION_MARKER, :STRING_BYTESIZE, :STRING_BYTESLICE, :MODULE_MATCH, :MODULE_NAME,
|
|
15
|
+
:OBJECT_CLASS
|
|
13
16
|
|
|
14
17
|
extend Dry::Initializer
|
|
15
18
|
|
|
@@ -18,6 +21,12 @@ module Smith
|
|
|
18
21
|
|
|
19
22
|
def self.capture(value, max_bytes: MAX_BYTES) = new(value, max_bytes:).call
|
|
20
23
|
|
|
24
|
+
# An error's class name, read through the native lookups so the error
|
|
25
|
+
# cannot misreport it, and bounded like any diagnostic identifier.
|
|
26
|
+
def self.error_class_name(error)
|
|
27
|
+
capture(MODULE_NAME.bind_call(OBJECT_CLASS.bind_call(error)) || "anonymous_error", max_bytes: 512)
|
|
28
|
+
end
|
|
29
|
+
|
|
21
30
|
def call
|
|
22
31
|
validate_limit!
|
|
23
32
|
text = utf8_prefix
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
module Doctor
|
|
7
|
+
module Checks
|
|
8
|
+
# The artifact store: configured, buildable, usable, and as durable as
|
|
9
|
+
# the workflow state, since a run that waits, retries or resumes in
|
|
10
|
+
# another process reads its artifacts there.
|
|
11
|
+
module Artifacts
|
|
12
|
+
PROCESS_LOCAL = %w[Smith::Artifacts::Memory].freeze
|
|
13
|
+
COLUMNS = %w[ref data content_type execution_namespace created_at].freeze
|
|
14
|
+
|
|
15
|
+
def self.run(report)
|
|
16
|
+
raw = ::Smith.config.artifact_store
|
|
17
|
+
return report.add(**missing) if raw.nil? && !::Smith.config.test_mode
|
|
18
|
+
|
|
19
|
+
store = ::Smith.artifact_backend
|
|
20
|
+
report.add(name: "artifacts.store", status: :pass, message: "Artifact store: #{store_name(store)}")
|
|
21
|
+
check_usable(report, store)
|
|
22
|
+
check_durability(report, store)
|
|
23
|
+
check_ownership(report, store)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
report.add(name: "artifacts.store", status: :fail, message: "Artifact store configuration is invalid",
|
|
26
|
+
detail: e.message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.store_name(store) = store.is_a?(Module) ? store.name : store.class.name
|
|
30
|
+
|
|
31
|
+
def self.missing
|
|
32
|
+
{ name: "artifacts.store", status: :warn, message: "No artifact store configured",
|
|
33
|
+
detail: "Set config.artifact_store to :s3 (S3, R2), :active_record, :file, or a store, with " \
|
|
34
|
+
"config.artifact_options; steps that store artifacts or read documents fail without one" }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The table an Active Record store writes, the directory a File
|
|
38
|
+
# store writes, the bucket an S3 store writes.
|
|
39
|
+
def self.check_usable(report, store)
|
|
40
|
+
problem = case store
|
|
41
|
+
when ::Smith::Artifacts::ActiveRecordStore then table_problem(store)
|
|
42
|
+
when ::Smith::Artifacts::File then directory_problem(store)
|
|
43
|
+
when ::Smith::Artifacts::S3 then bucket_problem(store)
|
|
44
|
+
end
|
|
45
|
+
return unless problem
|
|
46
|
+
|
|
47
|
+
report.add(name: "artifacts.usable", status: :warn, message: "The artifact store cannot be used",
|
|
48
|
+
detail: problem)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.table_problem(store)
|
|
52
|
+
missing = COLUMNS - store.model.column_names
|
|
53
|
+
"#{store.model.name} lacks the columns #{missing.join(", ")}" if missing.any?
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
"the store's model cannot be loaded: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.directory_problem(store)
|
|
59
|
+
dir = store.instance_variable_get(:@dir)
|
|
60
|
+
"the directory #{dir} is not writable" unless ::File.directory?(dir) && ::File.writable?(dir)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.bucket_problem(store)
|
|
64
|
+
client = store.instance_variable_get(:@client)
|
|
65
|
+
return unless client.respond_to?(:head_bucket)
|
|
66
|
+
|
|
67
|
+
client.head_bucket(bucket: store.instance_variable_get(:@bucket))
|
|
68
|
+
nil
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
"the bucket cannot be reached: #{e.class.name}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.check_durability(report, store)
|
|
74
|
+
adapter = ::Smith.config.persistence_adapter
|
|
75
|
+
return unless PROCESS_LOCAL.include?(store.class.name) && adapter && !process_local_state?(adapter)
|
|
76
|
+
|
|
77
|
+
report.add(
|
|
78
|
+
name: "artifacts.durability", status: :warn,
|
|
79
|
+
message: "Artifacts are kept in process memory while workflow state is persisted",
|
|
80
|
+
detail: "A run that waits, retries or resumes in another process loses its artifacts; configure :s3, " \
|
|
81
|
+
":active_record or :file"
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.process_local_state?(adapter)
|
|
86
|
+
adapter.to_s == "memory" || adapter.is_a?(::Smith::PersistenceAdapters::Memory)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.check_ownership(report, store)
|
|
90
|
+
return if store.respond_to?(:stored_by?)
|
|
91
|
+
|
|
92
|
+
report.add(
|
|
93
|
+
name: "artifacts.ownership", status: :warn,
|
|
94
|
+
message: "The artifact store cannot tell which run stored a ref (stored_by?)",
|
|
95
|
+
detail: "Runs read any ref under their prefix, and content parts cannot read documents from it"
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
private_class_method :store_name, :missing, :check_usable, :table_problem, :directory_problem, :bucket_problem,
|
|
99
|
+
:check_durability, :process_local_state?, :check_ownership
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -6,7 +6,6 @@ module Smith
|
|
|
6
6
|
module Configuration
|
|
7
7
|
def self.run(report)
|
|
8
8
|
check_logger(report)
|
|
9
|
-
check_artifact_store(report)
|
|
10
9
|
check_trace_adapter(report)
|
|
11
10
|
check_pricing(report)
|
|
12
11
|
end
|
|
@@ -21,16 +20,6 @@ module Smith
|
|
|
21
20
|
)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
def self.check_artifact_store(report)
|
|
25
|
-
configured = !::Smith.config.artifact_store.nil?
|
|
26
|
-
report.add(
|
|
27
|
-
name: "config.artifact_store",
|
|
28
|
-
status: configured ? :pass : :warn,
|
|
29
|
-
message: configured ? "Artifact store configured" : "No artifact store configured",
|
|
30
|
-
detail: configured ? nil : "Large outputs will use in-memory default"
|
|
31
|
-
)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
23
|
def self.check_trace_adapter(report)
|
|
35
24
|
configured = !::Smith.config.trace_adapter.nil?
|
|
36
25
|
report.add(
|
|
@@ -31,24 +31,34 @@ module Smith
|
|
|
31
31
|
)
|
|
32
32
|
return
|
|
33
33
|
else
|
|
34
|
+
check_ttl_support(report, adapter)
|
|
34
35
|
check_persist_and_restore(report, adapter)
|
|
35
36
|
check_resume_after_restore(report, adapter)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
# ActiveRecordStore refuses every write that carries a TTL, so a
|
|
40
|
+
# configured global TTL would fail the first persisted run.
|
|
41
|
+
def self.check_ttl_support(report, adapter)
|
|
42
|
+
ttl = ::Smith.config.persistence_ttl
|
|
43
|
+
return if ttl.nil? || !adapter.is_a?(::Smith::PersistenceAdapters::ActiveRecordStore)
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
report.add(
|
|
46
|
+
name: "durability.ttl",
|
|
47
|
+
status: :fail,
|
|
48
|
+
message: "ActiveRecordStore cannot expire workflow state; persistence_ttl must be nil",
|
|
49
|
+
detail: "Smith.config.persistence_ttl is #{ttl.inspect}, so every persisted write refuses it. " \
|
|
50
|
+
"Set it to nil, or use an adapter that honours TTL."
|
|
51
|
+
)
|
|
52
|
+
end
|
|
46
53
|
|
|
54
|
+
def self.check_persist_and_restore(report, adapter)
|
|
55
|
+
restored = probe_round_trip(adapter)
|
|
47
56
|
valid = restored.state == :idle
|
|
48
57
|
report.add(
|
|
49
58
|
name: "durability.persist_restore",
|
|
50
59
|
status: valid ? :pass : :fail,
|
|
51
|
-
message: valid ? "Host persistence round-trip works" : "Host persistence round-trip failed"
|
|
60
|
+
message: valid ? "Host persistence round-trip works" : "Host persistence round-trip failed",
|
|
61
|
+
detail: "Written as runs write, with #{probe_write(adapter)}"
|
|
52
62
|
)
|
|
53
63
|
rescue StandardError => e
|
|
54
64
|
report.add(
|
|
@@ -58,15 +68,7 @@ module Smith
|
|
|
58
68
|
end
|
|
59
69
|
|
|
60
70
|
def self.check_resume_after_restore(report, adapter)
|
|
61
|
-
|
|
62
|
-
payload = JSON.generate(workflow_class.new.to_state)
|
|
63
|
-
|
|
64
|
-
adapter.store(PROBE_KEY, payload)
|
|
65
|
-
restored_payload = adapter.fetch(PROBE_KEY)
|
|
66
|
-
restored = workflow_class.from_state(JSON.parse(restored_payload))
|
|
67
|
-
adapter.delete(PROBE_KEY)
|
|
68
|
-
|
|
69
|
-
result = restored.run!
|
|
71
|
+
result = probe_round_trip(adapter).run!
|
|
70
72
|
valid = result.state == :done
|
|
71
73
|
|
|
72
74
|
report.add(
|
|
@@ -79,6 +81,22 @@ module Smith
|
|
|
79
81
|
message: "Resume after restore failed", detail: e.message)
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
# A probe run stored as runs are (Workflow#persist!: store_versioned
|
|
85
|
+
# where the adapter has it, and persistence_max_bytes) and restored.
|
|
86
|
+
def self.probe_round_trip(adapter)
|
|
87
|
+
workflow_class = build_probe_class
|
|
88
|
+
adapter.delete(PROBE_KEY)
|
|
89
|
+
workflow_class.new.persist!(PROBE_KEY, adapter:)
|
|
90
|
+
workflow_class.restore(PROBE_KEY, adapter:)
|
|
91
|
+
ensure
|
|
92
|
+
adapter.delete(PROBE_KEY)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.probe_write(adapter)
|
|
96
|
+
versioned = ::Smith::PersistenceAdapters.supports?(adapter, :store_versioned)
|
|
97
|
+
versioned ? "store_versioned" : "plain store (the adapter has no store_versioned)"
|
|
98
|
+
end
|
|
99
|
+
|
|
82
100
|
def self.build_probe_class
|
|
83
101
|
Class.new(::Smith::Workflow) do
|
|
84
102
|
initial_state :idle
|
|
@@ -17,7 +17,10 @@ module Smith
|
|
|
17
17
|
OPTIONAL_CAPABILITIES = Smith::PersistenceAdapters::OPTIONAL_METHODS
|
|
18
18
|
|
|
19
19
|
def run(report)
|
|
20
|
-
adapter =
|
|
20
|
+
adapter = Smith.persistence_adapter
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
report_invalid_adapter(report, e)
|
|
23
|
+
else
|
|
21
24
|
return report_missing_adapter(report) if adapter.nil?
|
|
22
25
|
|
|
23
26
|
missing = OPTIONAL_CAPABILITIES.reject { |cap| capability_available?(adapter, cap) }
|
|
@@ -26,12 +29,6 @@ module Smith
|
|
|
26
29
|
report_missing_capabilities(report, adapter, missing)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
def resolve_adapter
|
|
30
|
-
Smith.persistence_adapter
|
|
31
|
-
rescue StandardError
|
|
32
|
-
nil
|
|
33
|
-
end
|
|
34
|
-
|
|
35
32
|
def capability_available?(adapter, capability)
|
|
36
33
|
return false unless Smith::PersistenceAdapters.supports?(adapter, capability)
|
|
37
34
|
return true unless capability == :persistence_identity
|
|
@@ -42,6 +39,15 @@ module Smith
|
|
|
42
39
|
false
|
|
43
40
|
end
|
|
44
41
|
|
|
42
|
+
def report_invalid_adapter(report, error)
|
|
43
|
+
report.add(
|
|
44
|
+
name: "persistence.capabilities",
|
|
45
|
+
status: :fail,
|
|
46
|
+
message: "Persistence adapter configuration is invalid",
|
|
47
|
+
detail: error.message
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
45
51
|
def report_missing_adapter(report)
|
|
46
52
|
report.add(
|
|
47
53
|
name: "persistence.capabilities",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
module Doctor
|
|
5
|
+
module Checks
|
|
6
|
+
# Whether an ActiveRecord persistence adapter's model can hold runs:
|
|
7
|
+
# its locking column, a text payload, and a string key the database
|
|
8
|
+
# keeps unique. It reads schema metadata only, and skips when the
|
|
9
|
+
# database cannot be reached (the default doctor run is offline).
|
|
10
|
+
# Other adapters, and an adapter configuration that does not resolve
|
|
11
|
+
# (Durability reports it), are not checked.
|
|
12
|
+
module StateModel
|
|
13
|
+
NAME = "persistence.state_model"
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def run(report)
|
|
18
|
+
adapter = active_record_adapter
|
|
19
|
+
return unless adapter
|
|
20
|
+
|
|
21
|
+
adapter.check_schema!
|
|
22
|
+
report.add(name: NAME, status: :pass,
|
|
23
|
+
message: "The workflow state model has a lock_version, a text payload and a unique key")
|
|
24
|
+
report_encryption(report) if adapter.encrypted_payload?
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
report_failure(report, e)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def report_failure(report, error)
|
|
30
|
+
if unreachable?(error)
|
|
31
|
+
report.add(name: NAME, status: :skip, detail: error.class.name,
|
|
32
|
+
message: "The workflow state model was not checked: the database could not be reached")
|
|
33
|
+
else
|
|
34
|
+
report.add(name: NAME, status: :fail, message: "The workflow state model cannot hold runs",
|
|
35
|
+
detail: error.message)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def report_encryption(report)
|
|
40
|
+
report.add(name: "#{NAME}.encryption", status: :warn,
|
|
41
|
+
message: "The workflow state payload is encrypted",
|
|
42
|
+
detail: "Runs store and read it, but split-step exact writes (replace_exact) compare " \
|
|
43
|
+
"payloads in the database and cannot use an encrypted one")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unreachable?(error)
|
|
47
|
+
classes = PersistenceAdapters::ActiveRecordConnectionErrors.classes
|
|
48
|
+
classes << ::ActiveRecord::NoDatabaseError if defined?(::ActiveRecord::NoDatabaseError)
|
|
49
|
+
classes.any? { error.is_a?(_1) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def active_record_adapter
|
|
53
|
+
return unless ::Smith.config.persistence_adapter
|
|
54
|
+
|
|
55
|
+
adapter = ::Smith.persistence_adapter
|
|
56
|
+
adapter if adapter.is_a?(::Smith::PersistenceAdapters::ActiveRecordStore)
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "erb"
|
|
3
4
|
require "fileutils"
|
|
4
5
|
|
|
5
6
|
module Smith
|
|
@@ -41,8 +42,9 @@ module Smith
|
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def rails_template
|
|
45
|
+
# The generator's initializer, as `rails g smith:install --skip-state-model` writes it.
|
|
44
46
|
template_path = File.expand_path("../../generators/smith/install/templates/smith.rb.tt", __dir__)
|
|
45
|
-
File.read(template_path)
|
|
47
|
+
ERB.new(File.read(template_path), trim_mode: "-").result_with_hash(options: { skip_state_model: true })
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def plain_template
|
|
@@ -78,7 +80,12 @@ module Smith
|
|
|
78
80
|
# store(key, payload)
|
|
79
81
|
# fetch(key)
|
|
80
82
|
# delete(key)
|
|
81
|
-
|
|
83
|
+
#
|
|
84
|
+
# Artifacts (large outputs, attached documents) need a store that
|
|
85
|
+
# outlives the process: :s3 (S3, R2) or :active_record, with
|
|
86
|
+
# config.artifact_options; :memory keeps them in this process.
|
|
87
|
+
# config.artifact_store = :s3
|
|
88
|
+
# config.artifact_options = { bucket: "smith-artifacts", region: "us-east-1" }
|
|
82
89
|
config.trace_adapter = Smith::Trace::Memory.new
|
|
83
90
|
end
|
|
84
91
|
RUBY
|
data/lib/smith/doctor.rb
CHANGED
|
@@ -15,6 +15,8 @@ require_relative "doctor/checks/live"
|
|
|
15
15
|
require_relative "doctor/checks/models_registry"
|
|
16
16
|
require_relative "doctor/checks/openai_api_mode"
|
|
17
17
|
require_relative "doctor/checks/persistence_capabilities"
|
|
18
|
+
require_relative "doctor/checks/artifacts"
|
|
19
|
+
require_relative "doctor/checks/state_model"
|
|
18
20
|
|
|
19
21
|
module Smith
|
|
20
22
|
module Doctor
|
|
@@ -23,6 +25,8 @@ module Smith
|
|
|
23
25
|
|
|
24
26
|
Checks::Baseline.run(report)
|
|
25
27
|
Checks::Configuration.run(report)
|
|
28
|
+
Checks::Artifacts.run(report)
|
|
29
|
+
Checks::StateModel.run(report)
|
|
26
30
|
Checks::ModelsRegistry.run(report)
|
|
27
31
|
Checks::OpenaiApiMode.run(report)
|
|
28
32
|
Checks::Rails.run(report) if detect_rails?
|