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
|
@@ -46,7 +46,10 @@ module Smith
|
|
|
46
46
|
# (execution key from the restored persistence key, the split
|
|
47
47
|
# step's transition, then the branch overlay downstream).
|
|
48
48
|
# Restoration is owned by within_raw_step_context's snapshot.
|
|
49
|
-
|
|
49
|
+
if @split_step_transition
|
|
50
|
+
install_step_attribution(@split_step_transition)
|
|
51
|
+
install_step_idempotency_scope(@split_step_transition)
|
|
52
|
+
end
|
|
50
53
|
with_scoped_artifacts(&block)
|
|
51
54
|
end
|
|
52
55
|
[output, nil]
|
|
@@ -33,8 +33,10 @@ module Smith
|
|
|
33
33
|
|
|
34
34
|
def prepare_composite_input(authorization, transition, specs)
|
|
35
35
|
ThreadContextSnapshot.new.around do
|
|
36
|
+
# The step's position, as in-process guardrails run with it.
|
|
37
|
+
install_step_idempotency_scope(transition)
|
|
36
38
|
run_composite_input_guardrails(authorization, transition, specs)
|
|
37
|
-
agent_messages =
|
|
39
|
+
agent_messages = step_prepared_input(transition)
|
|
38
40
|
Composite::Input.build(agent_messages:, session_messages: snapshot_session_messages)
|
|
39
41
|
end
|
|
40
42
|
end
|
|
@@ -70,10 +70,15 @@ module Smith
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def prepare_claimed_persisted_step!(key, adapter)
|
|
73
|
+
raise WorkflowError, "split-step persistence cannot prepare a step while the run waits" if waiting?
|
|
74
|
+
|
|
73
75
|
transition = pending_split_step_transition
|
|
74
76
|
transition_name = @next_transition_name || transition&.name
|
|
75
77
|
raise WorkflowError, "workflow has no pending transition" unless transition_name
|
|
76
78
|
|
|
79
|
+
refusal = "split-step persistence does not run steps that wait or launch runs"
|
|
80
|
+
raise WorkflowError, refusal if transition&.waits_or_launches?
|
|
81
|
+
|
|
77
82
|
ensure_transition_budget!
|
|
78
83
|
resolved_key = normalize_split_step_persistence_key(candidate_split_step_persistence_key(key))
|
|
79
84
|
store = persistence_adapter!(adapter)
|
|
@@ -58,6 +58,7 @@ module Smith
|
|
|
58
58
|
|
|
59
59
|
def detach_split_step_results!
|
|
60
60
|
@last_output = snapshot_value(@last_output)
|
|
61
|
+
@last_transcript_output = snapshot_value(@last_transcript_output)
|
|
61
62
|
@last_agent_execution = snapshot_value(@last_agent_execution)
|
|
62
63
|
@last_failed_step = snapshot_value(@last_failed_step)
|
|
63
64
|
@last_prepared_input = snapshot_value(@last_prepared_input)
|
|
@@ -3,26 +3,32 @@
|
|
|
3
3
|
module Smith
|
|
4
4
|
class Workflow
|
|
5
5
|
module StepCompletion
|
|
6
|
+
# Room for what completing a step adds besides its output (its state,
|
|
7
|
+
# step count, last agent).
|
|
8
|
+
RESULT_MARGIN = 1024
|
|
9
|
+
|
|
6
10
|
private
|
|
7
11
|
|
|
8
12
|
def complete_step(transition, output)
|
|
13
|
+
StepCompletion.instance_method(:check_result_storable!).bind_call(self, output)
|
|
9
14
|
step = { transition: transition.name, from: transition.from, to: transition.to, output: output }
|
|
10
15
|
# Fold in the serial agent attribution captured for this step (if any),
|
|
11
16
|
# then clear the transient carrier. record_step_snapshot reads these off
|
|
12
17
|
# the returned step to update the durable @last_agent_execution.
|
|
13
|
-
if @pending_agent_execution
|
|
14
|
-
step[:model] = @pending_agent_execution[:model]
|
|
15
|
-
step[:provider] = @pending_agent_execution[:provider]
|
|
16
|
-
end
|
|
18
|
+
step.merge!(@pending_agent_execution.slice(:model, :provider)) if @pending_agent_execution
|
|
17
19
|
|
|
18
20
|
@pending_agent_execution = nil
|
|
21
|
+
fold_pending_evaluations(step)
|
|
19
22
|
result = SplitStepPersistence
|
|
20
23
|
.instance_method(:prepare_split_step_execution_result)
|
|
21
24
|
.bind_call(self, step)
|
|
22
|
-
StepCompletion.instance_method(:
|
|
25
|
+
StepCompletion.instance_method(:keep_accepted_output).bind_call(self, transition, output)
|
|
23
26
|
SplitStepPersistence
|
|
24
27
|
.instance_method(:commit_split_step_execution_result!)
|
|
25
28
|
.bind_call(self, result)
|
|
29
|
+
# Cleared only once the record is committed: a completion that fails
|
|
30
|
+
# before this folds the same verdicts into its failure record.
|
|
31
|
+
@pending_evaluations = nil
|
|
26
32
|
@state = transition.to
|
|
27
33
|
@next_transition_name = @router_next_transition || transition.success_transition
|
|
28
34
|
@router_next_transition = nil
|
|
@@ -30,10 +36,37 @@ module Smith
|
|
|
30
36
|
step
|
|
31
37
|
end
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
# A stored run's step whose result would take its state past
|
|
40
|
+
# persistence_max_bytes fails with PersistedStateTooLarge before the
|
|
41
|
+
# result is kept, as with any error: its usage is recorded, its
|
|
42
|
+
# on_failure taken, and its run stays storable (strict mode
|
|
43
|
+
# included), where a refused store after the step would leave it
|
|
44
|
+
# unsaved. The estimate is the state's JSON and the output's, kept
|
|
45
|
+
# twice (in the context or transcript, and as the last output).
|
|
46
|
+
def check_result_storable!(output)
|
|
47
|
+
limit = Smith.config.persistence_max_bytes
|
|
48
|
+
return unless limit && @persistence_version.to_i.positive?
|
|
49
|
+
|
|
50
|
+
bytes = JSON.generate(to_state).bytesize + (JSON.generate(output).bytesize * 2) + RESULT_MARGIN
|
|
51
|
+
raise PersistedStateTooLarge.new(key: @persistence_key, bytes:, limit:) if bytes > limit
|
|
52
|
+
rescue JSON::GeneratorError, JSON::NestingError, Encoding::UndefinedConversionError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Once the result is captured, so a step whose result is refused leaves
|
|
57
|
+
# neither the context nor the transcript changed.
|
|
58
|
+
def keep_accepted_output(transition, output)
|
|
59
|
+
StepIO.instance_method(:deliver_output_to).bind_call(self, transition, output) if transition.output_to
|
|
60
|
+
StepCompletion.instance_method(:append_accepted_output).bind_call(self, transition, output)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The transcript keeps an accepted output unless the step writes it to
|
|
64
|
+
# the context (`output_to:`) or the session is isolated.
|
|
65
|
+
def append_accepted_output(transition, output)
|
|
66
|
+
return unless @session_messages && !output.nil? && transcript_output?(transition)
|
|
35
67
|
|
|
36
68
|
@session_messages << { role: :assistant, content: output }
|
|
69
|
+
@last_transcript_output = output
|
|
37
70
|
end
|
|
38
71
|
end
|
|
39
72
|
end
|
|
@@ -10,14 +10,17 @@ module Smith
|
|
|
10
10
|
|
|
11
11
|
def with_step_context(transition, &block)
|
|
12
12
|
ThreadContextSnapshot.new.around do
|
|
13
|
+
start_active_clock!
|
|
13
14
|
setup_step_context
|
|
14
15
|
install_step_attribution(transition)
|
|
16
|
+
install_step_idempotency_scope(transition)
|
|
15
17
|
Thread.handle_interrupt(Object => :immediate, &block)
|
|
16
18
|
rescue StandardError => e
|
|
17
19
|
@outcome = nil
|
|
18
20
|
GuardrailIntegration.instance_method(:handle_step_failure).bind_call(self, transition, e)
|
|
19
21
|
ensure
|
|
20
22
|
teardown_step_context
|
|
23
|
+
stop_active_clock!
|
|
21
24
|
end
|
|
22
25
|
ensure
|
|
23
26
|
flush_step_failure_emission
|
|
@@ -47,12 +50,17 @@ module Smith
|
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
|
|
53
|
+
# A durable composite branch runs here, in its own restored instance:
|
|
54
|
+
# it starts from what is left of the run's wall_clock, and its running
|
|
55
|
+
# time is not added back to the run's active_seconds.
|
|
50
56
|
def within_raw_step_context(&block)
|
|
51
57
|
ThreadContextSnapshot.new.around do
|
|
58
|
+
start_active_clock!
|
|
52
59
|
setup_step_context
|
|
53
60
|
Thread.handle_interrupt(Object => :immediate, &block)
|
|
54
61
|
ensure
|
|
55
62
|
teardown_step_context
|
|
63
|
+
stop_active_clock!
|
|
56
64
|
end
|
|
57
65
|
end
|
|
58
66
|
|
|
@@ -80,6 +88,19 @@ module Smith
|
|
|
80
88
|
)
|
|
81
89
|
end
|
|
82
90
|
|
|
91
|
+
# The step's position for idempotency keys: its number and transition
|
|
92
|
+
# in this run's namespace. A nested workflow sharing the namespace (an
|
|
93
|
+
# in-process child) is placed under the parent's current position, so
|
|
94
|
+
# a child step never takes a key a parent step already used; any other
|
|
95
|
+
# workflow started inside a step is a run of its own. Restoration is
|
|
96
|
+
# owned by the surrounding ThreadContextSnapshot.
|
|
97
|
+
def install_step_idempotency_scope(transition)
|
|
98
|
+
namespace = ArtifactIntegration.instance_method(:execution_namespace).bind_call(self)
|
|
99
|
+
outer = IdempotencyKey.__send__(:current_scope)
|
|
100
|
+
base = outer && outer.run == namespace ? outer : IdempotencyKey.__send__(:root_scope, namespace)
|
|
101
|
+
IdempotencyKey.__send__(:install, base.within(:step, @step_count + 1, transition.name.to_s))
|
|
102
|
+
end
|
|
103
|
+
|
|
83
104
|
def teardown_step_context
|
|
84
105
|
Tool.current_guardrails = nil
|
|
85
106
|
Tool.current_deadline = nil
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "execution_result_snapshot"
|
|
4
|
+
require_relative "message_batch"
|
|
5
|
+
require_relative "message_value_normalizer"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class Workflow
|
|
9
|
+
# Per-step input and output: a step's `input:` replaces the session
|
|
10
|
+
# transcript as its messages, `output_to:` writes its output to a context
|
|
11
|
+
# key instead of the transcript, and an isolated session keeps no
|
|
12
|
+
# transcript for agents at all. Also the run's untrusted keys: its own
|
|
13
|
+
# context's, and those of every workflow it is nested in.
|
|
14
|
+
module StepIO
|
|
15
|
+
INPUT_ROLES = %w[user assistant].freeze
|
|
16
|
+
INHERITED_UNTRUSTED_KEYS = :smith_inherited_untrusted_keys
|
|
17
|
+
private_constant :INPUT_ROLES, :INHERITED_UNTRUSTED_KEYS
|
|
18
|
+
|
|
19
|
+
# Builds a nested workflow with its parent's untrusted keys in force
|
|
20
|
+
# from construction, before its seed builder runs.
|
|
21
|
+
def self.with_inherited_untrusted_keys(keys)
|
|
22
|
+
outer = Thread.current[INHERITED_UNTRUSTED_KEYS]
|
|
23
|
+
Thread.current[INHERITED_UNTRUSTED_KEYS] = untrusted_key_names!(keys)
|
|
24
|
+
yield
|
|
25
|
+
ensure
|
|
26
|
+
Thread.current[INHERITED_UNTRUSTED_KEYS] = outer
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Context key names: at most 1,000, each a Symbol or String of 1 to 256
|
|
30
|
+
# characters, as Symbols.
|
|
31
|
+
def self.untrusted_key_names!(keys)
|
|
32
|
+
valid = keys.is_a?(Array) && keys.length <= 1_000 &&
|
|
33
|
+
keys.all? { (_1.is_a?(Symbol) || _1.is_a?(String)) && _1.length.between?(1, 256) }
|
|
34
|
+
raise WorkflowError, "untrusted keys must be up to 1,000 context key names" unless valid
|
|
35
|
+
|
|
36
|
+
keys.map(&:to_sym).uniq.freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The keys handed to the workflow under construction; taken once, so a
|
|
40
|
+
# workflow its constructor builds does not inherit them too.
|
|
41
|
+
def self.take_inherited_untrusted_keys
|
|
42
|
+
keys = Thread.current[INHERITED_UNTRUSTED_KEYS]
|
|
43
|
+
Thread.current[INHERITED_UNTRUSTED_KEYS] = nil
|
|
44
|
+
keys || [].freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def isolated_session? = self.class.session == :isolated
|
|
50
|
+
|
|
51
|
+
def untrusted_context_keys
|
|
52
|
+
manager = self.class.context_manager
|
|
53
|
+
own = manager.respond_to?(:untrusted_keys) ? manager.untrusted_keys : []
|
|
54
|
+
own | (@inherited_untrusted_keys || [])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The context without its untrusted keys, as `inject_state` formatters
|
|
58
|
+
# and the `seed_messages` builder read it.
|
|
59
|
+
def trusted_context_view
|
|
60
|
+
keys = untrusted_context_keys
|
|
61
|
+
keys.empty? || !@context.is_a?(Hash) ? @context : @context.except(*keys)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The messages an agent step sees: its `input:`, or else the session
|
|
65
|
+
# transcript (with injected state). An isolated session has no
|
|
66
|
+
# transcript to give: an agent step there must declare `input:`, and
|
|
67
|
+
# any other step gets nothing, so state is never injected.
|
|
68
|
+
def step_prepared_input(transition)
|
|
69
|
+
return step_input_messages(transition) if transition.step_input
|
|
70
|
+
return build_session&.prepare! unless isolated_session?
|
|
71
|
+
return unless transition.agent_step?
|
|
72
|
+
|
|
73
|
+
raise WorkflowError, "transition #{DiagnosticText.capture(transition.name.to_s, max_bytes: 256)} runs an " \
|
|
74
|
+
"agent in an isolated session and must declare input:"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A non-empty Array of at most MessageBatch::MAX_MESSAGES user and
|
|
78
|
+
# assistant messages, bounded and JSON-shaped like session messages.
|
|
79
|
+
# The callable gets a copy of the context, so it cannot change the
|
|
80
|
+
# run's.
|
|
81
|
+
def step_input_messages(transition)
|
|
82
|
+
messages = transition.step_input.call(snapshot_value(@context))
|
|
83
|
+
unless messages.is_a?(Array) && !messages.empty? && messages.length <= MessageBatch::MAX_MESSAGES
|
|
84
|
+
raise WorkflowError, "input: must return a non-empty Array of at most #{MessageBatch::MAX_MESSAGES} messages"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
MessageValueNormalizer.new(messages, label: "step input").call.map { |message| step_input_message(message) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def step_input_message(message)
|
|
91
|
+
return step_input_parts_message(message) if Message.parts?(message)
|
|
92
|
+
unless message.is_a?(Hash) && message.keys.sort == %w[content role] && INPUT_ROLES.include?(message["role"]) &&
|
|
93
|
+
!message["content"].nil?
|
|
94
|
+
raise WorkflowError, "each input: message must be { role: :user or :assistant, content: } with content"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
{ role: message["role"].to_sym, content: message["content"] }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A message with content parts (Smith::Message), user or assistant.
|
|
101
|
+
def step_input_parts_message(message)
|
|
102
|
+
checked = Message.check!(message)
|
|
103
|
+
raise WorkflowError, "an input: message with content parts must be a user or assistant message" unless
|
|
104
|
+
INPUT_ROLES.include?(checked["role"])
|
|
105
|
+
|
|
106
|
+
{ role: checked["role"].to_sym, content: checked["content"], parts: true }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Checked before the step runs, so a key the persist list refuses
|
|
110
|
+
# costs no agent call.
|
|
111
|
+
def writable_output_key!(transition)
|
|
112
|
+
key = transition.output_to
|
|
113
|
+
keys = declared_persist_keys
|
|
114
|
+
raise UndeclaredContextKey.new(key:, persisted_keys: keys) if keys && !keys.include?(key)
|
|
115
|
+
|
|
116
|
+
key
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The step's output, nil included, under `output_to:`: JSON-shaped and
|
|
120
|
+
# in the context's key shape, so it reads the same live and restored.
|
|
121
|
+
def deliver_output_to(transition, output)
|
|
122
|
+
key = writable_output_key!(transition)
|
|
123
|
+
value = MessageValueNormalizer.new(output, label: "output_to: value",
|
|
124
|
+
max_bytes: ExecutionResultSnapshot::MAX_BYTES).call
|
|
125
|
+
@context[key] = ContextKeys.normalize(value)
|
|
126
|
+
record_persisted_key!(key)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def transcript_output?(transition) = transition.output_to.nil? && !isolated_session?
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
# A step declared with `stream:` streams through its sink
|
|
6
|
+
# (Smith::Streaming), which hears the step's outcome: its accepted
|
|
7
|
+
# output, a durable retry's wait, or its failure. Every other step clears
|
|
8
|
+
# the stream, so none of its calls reach another step's sink; a nested
|
|
9
|
+
# workflow is a run of its own, with its own steps.
|
|
10
|
+
#
|
|
11
|
+
# The accepted output reaches the sink once the step is stored: at once
|
|
12
|
+
# for a run that is not stored (no persistence key), else when `persist!`
|
|
13
|
+
# stores it, or when the run moves on without storing it. A store that
|
|
14
|
+
# fails tells the sink the step failed, with the store's error.
|
|
15
|
+
module StepStream
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def with_step_stream(transition)
|
|
19
|
+
@step_stream = transition.stream &&
|
|
20
|
+
Streaming.open(transition.stream, execution_key: @persistence_key, transition: transition.name)
|
|
21
|
+
step = yield
|
|
22
|
+
if wait_pending?(step) then @step_stream&.suspend
|
|
23
|
+
elsif @step_stream then accept_streamed(@step_stream, step[:output])
|
|
24
|
+
end
|
|
25
|
+
step
|
|
26
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- the sink hears of every failure, then it goes on
|
|
27
|
+
@step_stream&.fail(e.class)
|
|
28
|
+
raise
|
|
29
|
+
ensure
|
|
30
|
+
@step_stream = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def accept_streamed(session, output)
|
|
34
|
+
return session.finish(output) unless @persistence_key
|
|
35
|
+
|
|
36
|
+
@streamed_output = [session, output]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def deliver_streamed_output
|
|
40
|
+
session, output = @streamed_output
|
|
41
|
+
@streamed_output = nil
|
|
42
|
+
session&.finish(output)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def discard_streamed_output(error_class)
|
|
46
|
+
session, = @streamed_output
|
|
47
|
+
@streamed_output = nil
|
|
48
|
+
session&.fail(error_class)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -20,6 +20,7 @@ module Smith
|
|
|
20
20
|
current_invocation_context
|
|
21
21
|
current_invocation_sequence
|
|
22
22
|
current_invocation
|
|
23
|
+
current_idempotency_scope
|
|
23
24
|
].freeze
|
|
24
25
|
THREAD_KEYS = %i[
|
|
25
26
|
smith_attribution
|
|
@@ -28,6 +29,7 @@ module Smith
|
|
|
28
29
|
smith_failed_agent_results
|
|
29
30
|
smith_last_agent_result
|
|
30
31
|
smith_parallel_agent_binding
|
|
32
|
+
smith_step_depth
|
|
31
33
|
].freeze
|
|
32
34
|
|
|
33
35
|
option :tool_attributes, default: proc { TOOL_ATTRIBUTES }
|