smith-agents 0.8.0 → 0.11.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 +433 -0
- data/README.md +16 -6
- data/docs/CONFIGURATION.md +26 -6
- data/docs/PATTERNS.md +59 -0
- data/docs/PERSISTENCE.md +5 -1
- data/lib/smith/agent/completion_usage_recording.rb +21 -3
- data/lib/smith/agent/fallback_configuration.rb +26 -3
- data/lib/smith/agent/invocation_preparation.rb +24 -0
- data/lib/smith/agent/lifecycle.rb +4 -2
- data/lib/smith/agent/provider_attempt.rb +17 -4
- data/lib/smith/agent/provider_call_timing.rb +129 -0
- data/lib/smith/agent/provider_completion.rb +26 -8
- data/lib/smith/agent/provider_failure_handling.rb +2 -2
- data/lib/smith/agent/provider_usage.rb +6 -0
- data/lib/smith/agent/usage_entry_recording.rb +27 -4
- data/lib/smith/agent/usage_traces.rb +49 -0
- data/lib/smith/agent/usage_tracking.rb +26 -20
- data/lib/smith/agent.rb +9 -1
- data/lib/smith/attribution.rb +115 -0
- data/lib/smith/budget/amount_representation.rb +20 -0
- data/lib/smith/budget/ledger.rb +6 -2
- data/lib/smith/context/state_injection.rb +12 -2
- data/lib/smith/diagnostic_text.rb +10 -1
- data/lib/smith/doctor/checks/durability.rb +16 -0
- data/lib/smith/doctor/checks/persistence_capabilities.rb +13 -7
- data/lib/smith/errors.rb +55 -4
- data/lib/smith/event.rb +10 -2
- data/lib/smith/events/bus.rb +63 -5
- data/lib/smith/events/step_completed.rb +3 -0
- data/lib/smith/events/step_failed.rb +25 -0
- data/lib/smith/events/subscription.rb +8 -0
- data/lib/smith/persistence_adapters/active_record_exact_store.rb +8 -3
- data/lib/smith/persistence_adapters/active_record_store.rb +4 -7
- data/lib/smith/persistence_adapters.rb +10 -0
- data/lib/smith/provider_permanent_failure.rb +44 -0
- data/lib/smith/tool/capture.rb +9 -1
- data/lib/smith/tool.rb +10 -1
- data/lib/smith/trace/logger.rb +1 -0
- data/lib/smith/trace/memory.rb +40 -4
- data/lib/smith/trace/open_telemetry.rb +47 -3
- data/lib/smith/trace.rb +24 -5
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/budget_integration.rb +3 -3
- data/lib/smith/workflow/composite/effects.rb +64 -33
- data/lib/smith/workflow/deterministic_execution.rb +3 -2
- data/lib/smith/workflow/deterministic_step.rb +15 -2
- data/lib/smith/workflow/dsl.rb +40 -6
- data/lib/smith/workflow/durability.rb +1 -1
- data/lib/smith/workflow/evaluator_optimizer.rb +111 -11
- data/lib/smith/workflow/event_integration.rb +48 -1
- data/lib/smith/workflow/execution.rb +10 -0
- data/lib/smith/workflow/failure_reconstructor.rb +18 -2
- data/lib/smith/workflow/failure_record.rb +6 -11
- data/lib/smith/workflow/failure_record_validator.rb +23 -3
- data/lib/smith/workflow/fanout_execution.rb +1 -1
- data/lib/smith/workflow/guardrail_integration.rb +27 -0
- data/lib/smith/workflow/optimization_state.rb +2 -2
- data/lib/smith/workflow/parallel_execution.rb +6 -0
- data/lib/smith/workflow/persistence.rb +55 -6
- data/lib/smith/workflow/prepared_branch_execution.rb +13 -4
- data/lib/smith/workflow/retry_execution.rb +4 -0
- data/lib/smith/workflow/run_result.rb +10 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +9 -1
- data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +1 -0
- data/lib/smith/workflow/step_completion.rb +13 -0
- data/lib/smith/workflow/step_context.rb +46 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +1 -0
- data/lib/smith/workflow/usage_entry.rb +32 -4
- data/lib/smith/workflow.rb +30 -3
- data/lib/smith.rb +9 -4
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3692cc78203afacc91c1ede53a150d8bc864d8be5b9f549739570b5fe823dde
|
|
4
|
+
data.tar.gz: 6f9981471f8f8ddf2c6fb4141bfe2b6d8631bd21aaf78215c4c87d311aa6d3f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 870199be5d2163608606ab5a3eacca29c967e49e3f1f3fd193d88f4bdd57e455e973f03346b2b9769387b363c77147aaeb78a614837e4a665fca1fd4cfc66dc8
|
|
7
|
+
data.tar.gz: d555df7da59578922841547c53607a81ab7262a427d80a2140e3b9072158764cc33acca058a1fb380b20242315aa5161ca8659c7a26107e35527baa9efea9391
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,439 @@ All notable changes to Smith are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Smith is pre-1.0 and under active development; expect occasional contract tightening between minor versions until 1.0.
|
|
6
6
|
|
|
7
|
+
## [0.11.0] - 2026-10-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `fallback_models` accepts a context block, matching the dynamic
|
|
12
|
+
`model { |context| ... }`. `fallback_models { |context| [...] }` is evaluated
|
|
13
|
+
once per agent invocation with the workflow context, and its entries pass
|
|
14
|
+
exactly the static form's validation: every entry needs an explicit
|
|
15
|
+
provider. An unqualified or invalid entry fails the step with
|
|
16
|
+
`Smith::WorkflowError` before any provider attempt, and that error is not
|
|
17
|
+
retried: `Smith::Errors.retryable?` is false for it, so a `retry_on` without
|
|
18
|
+
error classes, `retry_on Smith::AgentError`, and host retries built on
|
|
19
|
+
`Smith::Errors.retryable_classes` all leave it failed, unlike an invalid
|
|
20
|
+
`model` block result, which raises the retryable `Smith::AgentError`. The
|
|
21
|
+
chain keeps its de-duplication. Declaring entries and a block together raises
|
|
22
|
+
`ArgumentError`; a later static declaration clears the block and a later
|
|
23
|
+
block clears the static list; subclasses inherit the block. Graph
|
|
24
|
+
inspection, runtime readiness, and the doctor model check never call the
|
|
25
|
+
block: `fallback_models` returns `nil` for the block form and
|
|
26
|
+
`fallback_models_block` exposes the block. `resolve_fallback_models(context)`
|
|
27
|
+
evaluates the block, or returns the static qualified list (empty when none
|
|
28
|
+
is declared) when no block is configured. Resolution is `O(F)` time and
|
|
29
|
+
space for `F` returned entries. The static form is unchanged.
|
|
30
|
+
- Every `:provider_call` trace carries `agent_name`, the registered name of
|
|
31
|
+
the agent that made the attempt (the value its usage entries carry), so a
|
|
32
|
+
generator and an evaluator sharing one `optimize` round, and so one
|
|
33
|
+
transition and round, stay apart; it is omitted for an unregistered agent.
|
|
34
|
+
When the provider reported usage, the trace also carries `input_tokens` and
|
|
35
|
+
`output_tokens`: the totals of the attempt's usage entries, covering a
|
|
36
|
+
successful completion, a failed attempt's completed tool-loop prefix, and
|
|
37
|
+
the usage a failed attempt's error reported. Both are omitted when the
|
|
38
|
+
provider reported none. A trace adapter can therefore record a complete
|
|
39
|
+
attempt when it ends instead of waiting for a step checkpoint. Token counts
|
|
40
|
+
are metadata, never content: `trace_content false` does not hide them.
|
|
41
|
+
Hosts with a `trace_fields` allowlist for `:provider_call` must add these
|
|
42
|
+
keys to receive them. The fields are documented in `docs/CONFIGURATION.md`.
|
|
43
|
+
- `optimize`'s `before_eval` may reject a candidate. A returned Hash whose
|
|
44
|
+
`accept` (Symbol or String key) is `false` is that round's evaluation: it is
|
|
45
|
+
normalized and validated exactly as evaluator output (`feedback` required,
|
|
46
|
+
a numeric `score` when `improvement_threshold` is set), the evaluator is not
|
|
47
|
+
called for the round, and the loop continues as a rejection, so the
|
|
48
|
+
feedback reaches the generator's refinement turn and `on_exhaustion` applies
|
|
49
|
+
when rounds run out. Any other return value, including `nil` and a Hash
|
|
50
|
+
with `accept: true`, is ignored exactly as before and the evaluator judges.
|
|
51
|
+
A rejected round records only the generator's usage and `:provider_call`.
|
|
52
|
+
Documented in the Evaluator-Optimizer section of `docs/PATTERNS.md`.
|
|
53
|
+
- `optimize`'s verdicts are an output. Each valid round's evaluation is
|
|
54
|
+
recorded as `{ round:, source:, verdict: }`: the optimizer round (the one
|
|
55
|
+
usage entries and traces carry), `:evaluator` or `:before_eval` for the one
|
|
56
|
+
that gave it, and the evaluation as normalized, every field the evaluator's
|
|
57
|
+
schema declares included, as a frozen copy. Once its loop has run, the
|
|
58
|
+
step's record carries its verdicts under `:evaluations` whether the step
|
|
59
|
+
completed or failed (a completion that fails after the loop, such as a split
|
|
60
|
+
step's snapshot, included), the verdict that ends the loop included, each
|
|
61
|
+
also naming its `attempt`, the retry policy's attempt that ran the loop: a
|
|
62
|
+
step `retry_on` runs again starts its loop afresh with rounds from 0, and the
|
|
63
|
+
earlier attempt's verdicts stay before the new ones. `RunResult#evaluations`
|
|
64
|
+
lists every optimize step's verdicts in step, attempt and round order as
|
|
65
|
+
frozen records with the step's `transition`, and
|
|
66
|
+
`OptimizationState#evaluations` gives a callable a frozen copy of its own
|
|
67
|
+
loop's verdicts so far. A step that failed before its loop, and every other
|
|
68
|
+
step, gains no key; a nested workflow's verdicts stay on its own run.
|
|
69
|
+
Verdicts are content, so no trace carries them, and like `steps` they
|
|
70
|
+
belong to the run that executed the step: nothing is persisted and a
|
|
71
|
+
terminal restore has none. Before this, a rejection's feedback lived only in
|
|
72
|
+
the loop and an accepted verdict was discarded, so a host could not see why
|
|
73
|
+
a candidate was sent back. Documented in the Evaluator-Optimizer section of
|
|
74
|
+
`docs/PATTERNS.md`.
|
|
75
|
+
- `Smith::StepInProgressOnRestore` gains `state`, the persisted state the
|
|
76
|
+
interrupted step started from (a Symbol), and `transition`, the next
|
|
77
|
+
transition the payload records (such as a routed one), else `nil`, so a
|
|
78
|
+
host can tell which step died without reading Smith's payload. Both are set
|
|
79
|
+
wherever restore raises it and default to `nil` for any other construction;
|
|
80
|
+
the constructor stays backward compatible and the message gains only
|
|
81
|
+
` state=...` when the state is known.
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- Contract tightening: an `optimize` round's verdict, now recorded as an
|
|
86
|
+
output, must hold JSON values only (Hashes with String or Symbol keys,
|
|
87
|
+
Arrays, Strings, Symbols, Integers, finite Floats, true, false, nil), and a
|
|
88
|
+
step's verdicts together must fit the limits a split step's record is held
|
|
89
|
+
to (`ExecutionResultSnapshot`'s depth, node and byte caps). A round whose
|
|
90
|
+
verdict breaks either, most likely a rejection `before_eval` returned
|
|
91
|
+
holding an object or a very large feedback, now fails the step with
|
|
92
|
+
`Smith::WorkflowError` at that round, in every run mode, keeping the
|
|
93
|
+
verdicts recorded before it. Before, such a verdict was never recorded and
|
|
94
|
+
the loop went on.
|
|
95
|
+
- **Breaking:** a deterministic step's `write_outcome` is deep-symbolized
|
|
96
|
+
when written, matching restore. A live `RunResult#outcome_payload` (and
|
|
97
|
+
`RunResult#outcome`) now has Symbol keys at every depth, so a host reading
|
|
98
|
+
String keys from a live payload must switch to Symbol keys. Restored
|
|
99
|
+
payloads were already symbolized, so an outcome now reads identically live
|
|
100
|
+
and after a JSON restore. Persisted JSON is unchanged.
|
|
101
|
+
- Contract tightening: `ActiveRecordStore` refuses to write with a non-nil
|
|
102
|
+
TTL instead of silently ignoring it, since it has no expiry column.
|
|
103
|
+
`store`, `store_versioned`, and `replace_exact` raise `ArgumentError`
|
|
104
|
+
before touching the database. Reads and deletes take no TTL, and resolving
|
|
105
|
+
the adapter never refuses, so `restore`, `persisted_state_exists?`,
|
|
106
|
+
`clear_persisted!`, `stuck_for?`, and `heartbeat_age` keep working while a
|
|
107
|
+
TTL is configured. A global or per-workflow `persistence_ttl` with this
|
|
108
|
+
adapter is refused by the initial checkpoint that `run_persisted!` and
|
|
109
|
+
`advance_persisted!` write before their first step, so no step runs before
|
|
110
|
+
the refusal. A nil TTL (the default) is unaffected. `smith doctor
|
|
111
|
+
--durability` fails a new `durability.ttl` check when the configured
|
|
112
|
+
adapter is an `ActiveRecordStore` and `Smith.config.persistence_ttl` is
|
|
113
|
+
set. The README, `docs/CONFIGURATION.md`, and the `persistence_ttl` DSL
|
|
114
|
+
comment no longer claim this adapter honours TTL.
|
|
115
|
+
- Contract tightening: workflow and agent `budget` declarations raise
|
|
116
|
+
`ArgumentError`, naming the accepted keys, when given a key Smith does not
|
|
117
|
+
read; previously such a key was accepted and ignored. Workflow budgets
|
|
118
|
+
accept `total_tokens`, `token_limit`, `total_cost`, `tool_calls`, and
|
|
119
|
+
`wall_clock`; agent budgets accept `token_limit`, `cost`, `wall_clock`,
|
|
120
|
+
`tool_calls`, `total_tokens`, and `total_cost`. `wall_clock` is in seconds:
|
|
121
|
+
the README budget example used the ignored `wall_clock_ms: 30_000` and now
|
|
122
|
+
uses `wall_clock: 30`. The check is `O(K)` time and space for `K` declared
|
|
123
|
+
keys. State written by 0.10.0 keeps restoring after the host removes such a
|
|
124
|
+
key: 0.10.0 reserved every declared key at each agent call, so it could
|
|
125
|
+
persist a consumed entry like `"wall_clock_ms" => 0`, and restore now drops
|
|
126
|
+
consumed entries (String or Symbol keys) for dimensions the workflow no
|
|
127
|
+
longer declares. An undeclared dimension has no limit, so dropping it is
|
|
128
|
+
safe. The filter is `O(D + C)` for `D` declared and `C` consumed keys.
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
|
|
132
|
+
- `DeterministicStep#last_output` returned `nil` after a JSON restore,
|
|
133
|
+
because a restored session message carries a Symbol key with a String role
|
|
134
|
+
value. `last_output` now accepts a Symbol or String role under either key
|
|
135
|
+
form. Restored messages keep the shape 0.10.0 gives them (Symbol keys, JSON
|
|
136
|
+
values such as `role: "assistant"`).
|
|
137
|
+
- Restored failures keep their type. `ProviderPermanentFailure`,
|
|
138
|
+
`BudgetExceeded`, and `GuardrailFailed` get their own failure families
|
|
139
|
+
(`provider_permanent_failure`, `budget_exceeded`, `guardrail_failed`) where
|
|
140
|
+
they were `other`, and restore as their own classes where they restored as
|
|
141
|
+
`RuntimeError`. `BlankAgentOutputError` keeps the `agent_error` family and
|
|
142
|
+
restores as itself where it restored as a plain `AgentError`.
|
|
143
|
+
`ProviderPermanentFailure` and `BlankAgentOutputError` gain `details` (each
|
|
144
|
+
value bounded to 512 bytes) and `from_details`, so `provider`, `model_id`,
|
|
145
|
+
`source_error_class`, `agent_name`, and `model_used` survive a restore.
|
|
146
|
+
Details stay JSON-normalized, and `FailureRecordValidator` rejects malformed
|
|
147
|
+
details, or one of these classes claiming another family, with
|
|
148
|
+
`Smith::PersistedFailureInvalid`. Records written by earlier versions (these
|
|
149
|
+
classes under `other`, or `BlankAgentOutputError` without details) keep
|
|
150
|
+
restoring exactly as before. The new families also appear as `error_family`
|
|
151
|
+
on failed `:transition` traces and `StepFailed` events. Composite branch
|
|
152
|
+
failure evidence (`Workflow::Composite::ErrorEvidence`) still classifies
|
|
153
|
+
these three errors as `other`. Rollback: 0.10.0 checks `error_family` and
|
|
154
|
+
`error_cause_family` against its own family list, so any persisted payload
|
|
155
|
+
whose `last_failed_step`, or its cause, carries one of the new families
|
|
156
|
+
fails to restore under 0.10.0, whether the run is terminal or not; clear or
|
|
157
|
+
settle such runs before rolling back.
|
|
158
|
+
- A failed or aborted attempt's `:provider_call` trace now carries
|
|
159
|
+
`error_class`: the exception's class name, bounded to 512 bytes by the
|
|
160
|
+
diagnostic text helper (`anonymous_error` for an anonymous class). It also
|
|
161
|
+
carries `error_cause_class`, the class name of the error's direct cause
|
|
162
|
+
(`Exception#cause`, else the error's `wrapped_exception`), captured the same
|
|
163
|
+
way and omitted when there is no cause: a `Faraday::ConnectionFailed`
|
|
164
|
+
wraps both never-connected causes (`Errno::ECONNREFUSED`, `SocketError`,
|
|
165
|
+
`Net::OpenTimeout`) and lost-after-sending ones (`EOFError`,
|
|
166
|
+
`Errno::ECONNRESET`), and a host deciding whether an attempt may have been
|
|
167
|
+
billed needs to tell them apart. Messages never ride the trace. Hosts with a
|
|
168
|
+
`trace_fields` allowlist for `:provider_call` must add `:error_class` and
|
|
169
|
+
`:error_cause_class` to receive them.
|
|
170
|
+
- `inject_state` injects only non-blank text. A formatter returning `nil` or
|
|
171
|
+
whitespace-only text no longer adds a bare `[smith:injected-state]` system
|
|
172
|
+
message, and when a formatter that returned text now returns blank, the
|
|
173
|
+
stale injected-state message is removed from session history. Injection
|
|
174
|
+
remains one linear scan over the session.
|
|
175
|
+
- Restore and timestamp asymmetries. The `provider` in
|
|
176
|
+
`last_agent_execution`, read by `DeterministicStep#last_agent_provider`,
|
|
177
|
+
restores as the Symbol a live run holds where it restored as a String.
|
|
178
|
+
`created_at` is now written with microsecond precision (`iso8601(6)`), so a
|
|
179
|
+
`wall_clock` deadline no longer fires up to a second early; the early firing
|
|
180
|
+
affected live runs too, since the deadline is always computed from the
|
|
181
|
+
stored string. `updated_at` is written with the same precision at every
|
|
182
|
+
step, so it never reads earlier than `created_at`. Payloads with
|
|
183
|
+
whole-second timestamps still restore.
|
|
184
|
+
- `optimize` sends a structured candidate (a Hash or Array from an
|
|
185
|
+
`output_schema` generator) to the evaluator, and replays it as the
|
|
186
|
+
refinement round's assistant turn, as JSON, using the serialization Smith
|
|
187
|
+
applies at the provider boundary, where it sent Ruby `inspect` notation.
|
|
188
|
+
String candidates are unchanged.
|
|
189
|
+
- A budget reservation of the reported remaining amount no longer raises
|
|
190
|
+
`BudgetExceeded` far below the limit. `Budget::Ledger#remaining` returned
|
|
191
|
+
the Float nearest the exact remaining amount, and reserving it re-reads its
|
|
192
|
+
shortest decimal form, which after Float-priced costs sits above the exact
|
|
193
|
+
amount about half the time: a serial step on a $0.50 `total_cost` budget
|
|
194
|
+
failed its third call at $0.0707. `remaining` now returns a value that
|
|
195
|
+
never reads back above the exact amount, so it may read one ulp lower than
|
|
196
|
+
before. `Budget::Ledger#remaining_share(key, parts)` is a new public method
|
|
197
|
+
that returns one of `parts` equal shares that all fit, which fan-out branch
|
|
198
|
+
estimates now use (a `total_cost: 0.2` budget split across three branches
|
|
199
|
+
failed with nothing spent). Integers keep floor division; a Float steps
|
|
200
|
+
down in `O(1)` ulp steps. Amounts stay Integer or Float in and JSON-safe
|
|
201
|
+
numerics out.
|
|
202
|
+
- `smith doctor`'s `persistence.capabilities` check reports an adapter that
|
|
203
|
+
fails to resolve as a failed "Persistence adapter configuration is invalid"
|
|
204
|
+
check carrying the resolution error, where it reported "No persistence
|
|
205
|
+
adapter configured".
|
|
206
|
+
- A swallowed `on_step:` callback error is logged with its class after the
|
|
207
|
+
message (`Smith::Workflow on_step callback error: <message> (<class>)`),
|
|
208
|
+
where only the message was logged. It is still swallowed.
|
|
209
|
+
|
|
210
|
+
## [0.10.0] - 2026-08-24
|
|
211
|
+
|
|
212
|
+
### Added
|
|
213
|
+
|
|
214
|
+
- Last-serial-agent-execution attribution. A deterministic (`compute`) step can
|
|
215
|
+
now read the model and provider that actually served the most recent serial
|
|
216
|
+
`execute :agent` step via `DeterministicStep#last_agent_model` /
|
|
217
|
+
`#last_agent_provider` (symmetric with `#last_output`, which returns that
|
|
218
|
+
step's content). The value is the resolved model/provider after fallback
|
|
219
|
+
resolution, so a step that failed its primary and completed on a
|
|
220
|
+
`fallback_models` entry reports the model that actually ran, never the
|
|
221
|
+
configured primary. It is durable lifecycle state (persisted in `to_state` as
|
|
222
|
+
`last_agent_execution`, restored across crash/resume), not a trace scrape, and
|
|
223
|
+
is `nil` until a serial agent step runs. Backward compatible: pre-upgrade
|
|
224
|
+
states have no `last_agent_execution` key and restore to `nil`; old readers
|
|
225
|
+
slice off the unknown key.
|
|
226
|
+
|
|
227
|
+
### Fixed
|
|
228
|
+
|
|
229
|
+
- Structured agent outputs recorded as session messages (Hash or Array
|
|
230
|
+
content) are now serialized to JSON at the RubyLLM provider boundary when
|
|
231
|
+
replayed to a later agent step. RubyLLM treats a Hash message content as
|
|
232
|
+
attachments and opens each value as a file, so replaying a prior structured
|
|
233
|
+
output to the next agent in a workflow session raised `Errno::ENOENT`. The
|
|
234
|
+
session store is unaffected: `last_output` and persisted `session_messages`
|
|
235
|
+
keep the raw structured value; only the provider-facing copy is serialized.
|
|
236
|
+
|
|
237
|
+
## [0.9.0] - 2026-08-03
|
|
238
|
+
|
|
239
|
+
### Upgrade notes
|
|
240
|
+
|
|
241
|
+
- Rollback after running fan-out branches under this version is a one-way
|
|
242
|
+
door for those runs: composite branch effects written with attribution
|
|
243
|
+
values (usage entries) or batch-correlated `tool_call_id` capture entries
|
|
244
|
+
fail an older gem's exact-key validation at reduction or recovery.
|
|
245
|
+
Drain in-flight composite runs before rolling the gem back; plain
|
|
246
|
+
checkpoint payloads are unaffected (restored pre-attribution documents
|
|
247
|
+
re-serialize byte-identically, and old readers slice off unknown keys).
|
|
248
|
+
- Hosts with a configured trace adapter see new output on upgrade without
|
|
249
|
+
any host change: one `:provider_call` line per provider attempt, one
|
|
250
|
+
`:cost` line per priced completed invocation, failed `:transition` lines
|
|
251
|
+
marked `outcome: :failed`, and ambient attribution keys (including
|
|
252
|
+
`execution_key`, the Smith persistence key) merged into every payload.
|
|
253
|
+
Identifier-only, but plan for the volume and shape change, especially
|
|
254
|
+
with `Smith::Trace::Logger` in production.
|
|
255
|
+
- Hosts with a `trace_fields` allowlist for `:transition` must add
|
|
256
|
+
`:outcome` (and the error keys they want) or failed transitions render
|
|
257
|
+
indistinguishable from successes under the allowlist.
|
|
258
|
+
- `Smith::Trace::Memory` is now bounded (default 10,000 entries, silent
|
|
259
|
+
drop with `dropped_count`); previously it accumulated without limit.
|
|
260
|
+
|
|
261
|
+
### Added
|
|
262
|
+
|
|
263
|
+
- Add `Smith::Attribution`, an immutable thread-local execution attribution
|
|
264
|
+
context (`execution_key`, `transition`, `from`, `to`, `branch_key`,
|
|
265
|
+
`round`). Workflow execution installs it per step (the `execution_key`
|
|
266
|
+
defaults to the persistence key of a persisted run), fan-out carries it
|
|
267
|
+
into branch threads and overlays the branch key, and evaluator-optimizer
|
|
268
|
+
rounds overlay the round index. Hosts can seed an outer scope with
|
|
269
|
+
`Smith::Attribution.with(execution_key: ...)` around non-persisted runs.
|
|
270
|
+
Restoration inside workflow execution rides `ThreadContextSnapshot`, which
|
|
271
|
+
now tracks the attribution thread key. Scope overlays are nil-ignoring
|
|
272
|
+
(`Context#merge`), but the per-step facts (`transition`, `from`, `to`)
|
|
273
|
+
are replaced verbatim, nil included (`Context#override`): a nested
|
|
274
|
+
child's `from`-less transition never inherits the parent step's `from`,
|
|
275
|
+
and the failed `:transition` trace keeps `from`/`to` present even when
|
|
276
|
+
nil for the same reason.
|
|
277
|
+
- Merge ambient attribution fields into every `Smith::Trace.record` payload.
|
|
278
|
+
Attribution keys are identifiers, not content: caller-supplied keys win on
|
|
279
|
+
conflict, the content policy is unaffected, and a configured
|
|
280
|
+
`trace_fields` allowlist stays authoritative (add attribution keys to an
|
|
281
|
+
allowlist to receive them). Disable with `Smith.config.trace_attribution =
|
|
282
|
+
false` (default true).
|
|
283
|
+
- Bound `Smith::Trace::Memory` (default 10,000 entries) with a
|
|
284
|
+
`dropped_count` reader and a `snapshot` method for readers racing
|
|
285
|
+
concurrent recording.
|
|
286
|
+
- Tag `Workflow::UsageEntry` with the ambient attribution at recording time:
|
|
287
|
+
new optional members `transition`, `branch_key`, `round`, and `attempt_id`.
|
|
288
|
+
All four are nil on entries restored from checkpoints written by earlier
|
|
289
|
+
Smith versions and are omitted from serialization when nil, so restored
|
|
290
|
+
pre-attribution documents re-serialize byte-identically (hosts that digest
|
|
291
|
+
whole persisted documents in exact-mutation proofs depend on this). A
|
|
292
|
+
rolled-back gem drops the new keys from plain checkpoint payloads
|
|
293
|
+
(`from_h` slices to known members). Composite branch effects are the
|
|
294
|
+
exception: effects written by this version from a fan-out branch carry
|
|
295
|
+
real attribution values, and an older gem's exact-key effects validation
|
|
296
|
+
rejects them, so see the upgrade notes below before rolling back.
|
|
297
|
+
`recorded_at` now carries microsecond precision (`iso8601(6)`) on new
|
|
298
|
+
entries. Recording symbolizes `transition`/`branch_key` Strings exactly
|
|
299
|
+
as `from_h` does on restore, so a host seeding String attribution through
|
|
300
|
+
`Smith::Attribution.with` gets entries equal to their restored form.
|
|
301
|
+
- Measure each provider attempt with a monotonic clock around the whole chat
|
|
302
|
+
completion (including any provider tool loop) and emit one
|
|
303
|
+
`:provider_call` trace per attempt (success or failure) carrying `model`,
|
|
304
|
+
`provider`, `duration_ms`, `attempt_id`, `attempt_index`, and `outcome`.
|
|
305
|
+
Every usage entry the attempt produced shares its `attempt_id` (an attempt
|
|
306
|
+
with an N-round tool loop records N entries): join there for the attempt's
|
|
307
|
+
single duration, never sum across entries. Gate with
|
|
308
|
+
`Smith.config.trace_provider_calls` (default true). `ProviderAttempt`
|
|
309
|
+
gains optional `attempt_id` and `duration_ms`.
|
|
310
|
+
- Add a public read-only `Workflow#usage_entries` (frozen copy under the
|
|
311
|
+
recording mutex) so hosts can diff usage across a step boundary without a
|
|
312
|
+
full `to_state` serialization; `to_state` and `snapshot_usage_entries` now
|
|
313
|
+
read the ledger under the same mutex, so a state written mid-fan-out never
|
|
314
|
+
captures a torn array.
|
|
315
|
+
- Thread real correlation identity through events: `Smith::Event#execution_id`
|
|
316
|
+
and `#trace_id` default to the ambient attribution execution key (the
|
|
317
|
+
persistence key during persisted runs) instead of a fresh random UUID per
|
|
318
|
+
event; the random fallback remains for events built outside any execution
|
|
319
|
+
scope. The `:tool_call` trace gains a nullable `tool_call_id` when the
|
|
320
|
+
invocation came from a provider batch (the tool-results capture entry
|
|
321
|
+
gains the same key under a later bullet in this release; composite
|
|
322
|
+
effects accept the extended shape with bounded value validation).
|
|
323
|
+
|
|
324
|
+
- Emit from the step-failure paths, closing the success-only observation
|
|
325
|
+
gap: both `handle_step_failure` and the unresolved-transition handler now
|
|
326
|
+
record a `:transition` trace with `outcome: :failed` plus bounded
|
|
327
|
+
classification (`error_class`, `error_family` from FailureRecord's
|
|
328
|
+
taxonomy, `retryable`) and emit a new `Smith::Events::StepFailed` event.
|
|
329
|
+
Raw error messages never ride either; an emission failure is logged and
|
|
330
|
+
can never mask the original step error. The marker key is `outcome`
|
|
331
|
+
because `result` is a reserved content key in the trace pipeline. An
|
|
332
|
+
unresolved transition with no `:fail` transition still re-raises without
|
|
333
|
+
emitting: that path was never treated as a step. The unresolved handler
|
|
334
|
+
runs outside any step context, so it seeds the run identity explicitly;
|
|
335
|
+
both failure paths stamp persisted-run events with the persistence key.
|
|
336
|
+
Emission is terminal-per-step: a step that retries internally and then
|
|
337
|
+
succeeds emits only its `StepCompleted`; intermediate step-body retry
|
|
338
|
+
attempts stay dark at the step layer (provider-level failures remain
|
|
339
|
+
visible as `:provider_call` failure traces). `StepFailed` handlers run
|
|
340
|
+
outside the step snapshot's interrupt-masked region (emission is staged
|
|
341
|
+
in the failure rescue and flushed after the mask closes), interruptible
|
|
342
|
+
exactly like `StepCompleted` handlers. A step body that surfaces Smith's
|
|
343
|
+
own `UnresolvedTransitionError` emits exactly one `StepFailed` under the
|
|
344
|
+
real step identity; the unresolved handler recognizes the already-emitted
|
|
345
|
+
error instead of emitting a second event under the requested (never
|
|
346
|
+
executed) name.
|
|
347
|
+
- Emit the long-advertised `:cost` trace: one per completed agent
|
|
348
|
+
invocation, whose value is the sum of that invocation's per-response
|
|
349
|
+
usage-entry costs. Summing per response is what tiered catalogs actually
|
|
350
|
+
bill; pricing the aggregate token totals as one call would resolve the
|
|
351
|
+
wrong tier for multi-response tool loops. Emitted only for fully metered,
|
|
352
|
+
fully priced invocations (a partially priced or partially metered
|
|
353
|
+
invocation emits nothing rather than presenting an incomplete figure);
|
|
354
|
+
gated by the existing `trace_cost` setting (the per-type gates live in
|
|
355
|
+
the built-in adapters; a custom adapter receives every type). `:cost`
|
|
356
|
+
traces are not a spend total: billed failed and partial attempts appear
|
|
357
|
+
only in usage entries. The same per-response sum now becomes
|
|
358
|
+
`agent_result.cost`, so budget settlement, result surfaces, recorded
|
|
359
|
+
entries, and the trace all agree on one invocation cost.
|
|
360
|
+
- The OpenTelemetry adapter now creates retroactive spans with real
|
|
361
|
+
durations (span start backdated by `:tool_call` seconds or
|
|
362
|
+
`:provider_call` milliseconds; instant spans otherwise), preserves
|
|
363
|
+
numeric attribute types instead of stringifying everything, and uses only
|
|
364
|
+
the documented opentelemetry-api surface (`Tracer#start_span` with
|
|
365
|
+
`start_timestamp`, `Span#finish` with `end_timestamp`).
|
|
366
|
+
|
|
367
|
+
- Add a `workflow` discriminator to the ambient attribution, every trace
|
|
368
|
+
payload, usage entries, and the `StepCompleted`/`StepFailed` events: the
|
|
369
|
+
emitting workflow's class name ("anonymous" when unnamed), so
|
|
370
|
+
nested-child graph facts are distinguishable from parent facts under the
|
|
371
|
+
shared root execution identity. Nil-omitted from serialized entries like
|
|
372
|
+
the other attribution members.
|
|
373
|
+
- Every provider attempt now emits its `:provider_call` trace: `outcome` is
|
|
374
|
+
`:success`, `:failure` (provider failure, fallback may continue), or
|
|
375
|
+
`:aborted` (a non-provider error that re-raises), so prefix-accounted
|
|
376
|
+
usage entries always have their attempt join target.
|
|
377
|
+
- The tool-results capture entry gains `tool_call_id` when the invocation
|
|
378
|
+
came from a provider batch (omitted otherwise, so direct-invocation and
|
|
379
|
+
pre-existing payloads keep their exact two-key shape); composite effects
|
|
380
|
+
accept the extended shape while still rejecting unknown keys.
|
|
381
|
+
- `StepFailed` handlers now run outside the step snapshot's
|
|
382
|
+
interrupt-masked region: emission is staged in the failure rescue and
|
|
383
|
+
flushed after the mask closes with explicitly seeded run identity, so a
|
|
384
|
+
slow host handler can no longer make the workflow thread unkillable and
|
|
385
|
+
handlers match `StepCompleted`'s interruptibility.
|
|
386
|
+
|
|
387
|
+
### Fixed
|
|
388
|
+
|
|
389
|
+
- A user-declared `:fail` transition no longer inherits the early order
|
|
390
|
+
position of the auto-generated placeholder created by `state :failed`:
|
|
391
|
+
redeclaring a generated transition takes a fresh declaration position, so
|
|
392
|
+
it can no longer shadow a same-origin primary transition at run time.
|
|
393
|
+
Genuine user redefinitions keep their original position, and subclasses
|
|
394
|
+
inherit the bookkeeping.
|
|
395
|
+
- Budget cost settlement now consumes the per-response priced sum instead
|
|
396
|
+
of pricing the invocation's aggregate token totals. Under linear pricing
|
|
397
|
+
the figures are identical; under tiered pricing the aggregate resolved
|
|
398
|
+
the wrong tier (or missed every tier and settled zero), so a
|
|
399
|
+
cost-budgeted workflow could keep spending after its real billed cost
|
|
400
|
+
exceeded the budget.
|
|
401
|
+
|
|
402
|
+
### Removed
|
|
403
|
+
|
|
404
|
+
- Remove the never-read `trace_retention` and `trace_tenant_isolation`
|
|
405
|
+
settings. Both were silent no-ops since introduction; reading or writing
|
|
406
|
+
them now raises, so a host relying on the illusion fails loudly instead
|
|
407
|
+
of silently.
|
|
408
|
+
|
|
409
|
+
### Changed
|
|
410
|
+
|
|
411
|
+
- Workflow step failure is now observable: subscribers to the events bus
|
|
412
|
+
receive `StepFailed` where previously failures emitted nothing (the
|
|
413
|
+
success-only scope is gone), and trace consumers see failed `:transition`
|
|
414
|
+
payloads distinguished by `outcome: :failed`.
|
|
415
|
+
- `Workflow::Composite::Effects` validates usage-entry keys as
|
|
416
|
+
required-plus-allowed instead of exact: entries from an older producer
|
|
417
|
+
(missing the optional attribution keys) stay valid, current entries with
|
|
418
|
+
attribution pass, and unknown keys still reject. The optional keys are
|
|
419
|
+
bounded values, not just bounded keys: `transition`, `branch_key`, and
|
|
420
|
+
`workflow` must be non-empty Strings up to 256 characters, `round` a
|
|
421
|
+
non-negative Integer, and `attempt_id` a UUID when present. Tool results
|
|
422
|
+
accept the extended capture shape: `tool_call_id`, when present, must be
|
|
423
|
+
a non-empty String up to 1024 characters (`tool` keeps its exact prior
|
|
424
|
+
validation).
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
- `Smith::Events` subscriptions now live in per-class buckets guarded by a
|
|
428
|
+
mutex: emit touches only the buckets for the event's ancestors instead of
|
|
429
|
+
scanning every subscription, dispatch order remains registration order,
|
|
430
|
+
and `is_a?` matching semantics are unchanged (instance-extended modules
|
|
431
|
+
dispatch through the singleton class; immediate values, which have no
|
|
432
|
+
singleton class and cannot be extended, dispatch through their class
|
|
433
|
+
ancestors). `Subscription#cancel` now
|
|
434
|
+
detaches from the registry, so cancelled subscriptions (including
|
|
435
|
+
`Events.within` scopes) no longer leak. Handlers run outside the registry
|
|
436
|
+
lock, so a handler may subscribe or cancel without deadlocking.
|
|
437
|
+
- `Smith::Trace::Memory#record` and `#clear!` are mutex-guarded and safe
|
|
438
|
+
under parallel fan-out branches.
|
|
439
|
+
|
|
7
440
|
## [0.8.0] - 2026-07-25
|
|
8
441
|
|
|
9
442
|
### Added
|
data/README.md
CHANGED
|
@@ -217,10 +217,12 @@ message batch, appends it under Smith's workflow lifecycle lock, and returns an
|
|
|
217
217
|
immutable digest witness. The host still owns session identity, transaction
|
|
218
218
|
coordination, persistence, idempotency, and resume policy.
|
|
219
219
|
|
|
220
|
-
Built-in adapters (all support TTL
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
Built-in adapters (all except `ActiveRecord` support TTL; `ActiveRecord` has no
|
|
221
|
+
expiry column, so its writes refuse a non-nil `persistence_ttl`, while reads and
|
|
222
|
+
deletes keep working and `smith doctor --durability` flags the combination;
|
|
223
|
+
`Redis`, `ActiveRecord`, and `Memory` support optimistic locking via
|
|
224
|
+
`store_versioned` and exact dispatch claims via `replace_exact`; `ActiveRecord`
|
|
225
|
+
participates in the caller's transaction for host-record coordination;
|
|
224
226
|
`Redis` and `Memory` also support heartbeat probes via `record_heartbeat` /
|
|
225
227
|
`last_heartbeat`):
|
|
226
228
|
|
|
@@ -407,10 +409,16 @@ Guardrails run as input/output gates around agent calls. See [`docs/TOOLS_AND_GU
|
|
|
407
409
|
|
|
408
410
|
```ruby
|
|
409
411
|
class BudgetedWorkflow < Smith::Workflow
|
|
410
|
-
budget total_tokens: 10_000, total_cost: 0.50,
|
|
412
|
+
budget total_tokens: 10_000, total_cost: 0.50, wall_clock: 30
|
|
411
413
|
end
|
|
412
414
|
```
|
|
413
415
|
|
|
416
|
+
`wall_clock` is in seconds. A workflow budget accepts `total_tokens`,
|
|
417
|
+
`token_limit`, `total_cost`, `tool_calls`, and `wall_clock`; an agent budget
|
|
418
|
+
accepts `token_limit`, `cost`, `wall_clock`, `tool_calls`, `total_tokens`, and
|
|
419
|
+
`total_cost`. Any other key raises `ArgumentError` naming the accepted keys. A
|
|
420
|
+
restored run drops consumed amounts for keys the workflow no longer declares.
|
|
421
|
+
|
|
414
422
|
Budgets reserve serially at each step and reconcile after the agent call. Parallel branches reserve scoped envelopes that release back to the parent ledger. The `Workflow::RunResult` carries `total_tokens`, `total_cost`, and per-call `usage_entries`.
|
|
415
423
|
|
|
416
424
|
Direct ledger integrations settle the exact receipt returned by reservation:
|
|
@@ -422,7 +430,9 @@ ledger.reconcile_many!(reservation, actual: { total_tokens: 640, total_cost: 0.0
|
|
|
422
430
|
|
|
423
431
|
`release!` / `release_many!` accept the same receipt when work never starts.
|
|
424
432
|
Receipts are ledger-owned and one-shot: another ledger, a replay, or an
|
|
425
|
-
amount-only settlement is rejected.
|
|
433
|
+
amount-only settlement is rejected. `ledger.remaining(key)` never reads back
|
|
434
|
+
above the exact remaining amount, and `ledger.remaining_share(key, parts)`
|
|
435
|
+
returns one of `parts` equal shares of it such that reserving all `parts` fits.
|
|
426
436
|
|
|
427
437
|
Budget amounts accept finite, non-negative `Integer` and `Float` values. Smith
|
|
428
438
|
uses an exact `BigDecimal` representation derived from each value's canonical
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -83,8 +83,8 @@ Smith.configure do |config|
|
|
|
83
83
|
tool_call: %i[tool duration]
|
|
84
84
|
}
|
|
85
85
|
config.trace_content = false
|
|
86
|
-
config.
|
|
87
|
-
config.
|
|
86
|
+
config.trace_provider_calls = true
|
|
87
|
+
config.trace_attribution = true
|
|
88
88
|
|
|
89
89
|
config.pricing = {
|
|
90
90
|
%w[openai gpt-4.1-nano] => {
|
|
@@ -115,16 +115,16 @@ observes a provider but finds only a model-only rate, it raises
|
|
|
115
115
|
| `trace_transitions` | Emit transition traces | Usually leave on |
|
|
116
116
|
| `trace_tool_calls` | Emit tool call traces | Usually leave on |
|
|
117
117
|
| `trace_token_usage` | Emit usage traces | Useful for budget visibility |
|
|
118
|
-
| `
|
|
118
|
+
| `trace_provider_calls` | Emit one timed `:provider_call` trace per provider attempt | Usually leave on |
|
|
119
|
+
| `trace_cost` | Emit one `:cost` trace per completed agent invocation | Emits only once pricing is configured |
|
|
119
120
|
| `trace_fields` | Allowlist structural trace fields | Use when you want tighter trace output |
|
|
120
121
|
| `trace_content` | Whether content appears in traces | Leave `false` first |
|
|
121
|
-
| `
|
|
122
|
-
| `trace_tenant_isolation` | Trace multi-tenant isolation flag | Enable in multi-tenant systems |
|
|
122
|
+
| `trace_attribution` | Merge ambient execution attribution into trace payloads | Usually leave on |
|
|
123
123
|
| `pricing` | Best-known model-call cost catalog | Add once you care about `total_cost` |
|
|
124
124
|
| `logger` | Smith's runtime logger | Usually the first setting to add |
|
|
125
125
|
| `persistence_adapter` | Adapter for durable workflow state | `:redis`, `:rails_cache`, `:active_record`, `:memory`, or a custom object |
|
|
126
126
|
| `persistence_options` | Per-adapter options (client, namespace, model, columns) | See "Built-In Persistence Adapters" |
|
|
127
|
-
| `persistence_ttl` | Global TTL for persisted state (Integer/Float seconds; nil = no expiry) | Set when long-tail abandoned workflows accumulate in storage |
|
|
127
|
+
| `persistence_ttl` | Global TTL for persisted state (Integer/Float seconds; nil = no expiry). `:active_record` cannot expire state, so its writes refuse a non-nil TTL (reads and deletes still work, and `smith doctor --durability` flags the combination) | Set when long-tail abandoned workflows accumulate in storage |
|
|
128
128
|
| `persistence_retry_policy` | Exponential-backoff policy for transient adapter I/O failures | Defaults to `{ attempts: 3, base_delay: 0.1, max_delay: 1.0 }` |
|
|
129
129
|
| `retry_attempt_limit` | Maximum attempts accepted by workflow and persistence retry policies | Defaults to `100` |
|
|
130
130
|
| `parallel_branch_limit` | Maximum branches accepted by one homogeneous or heterogeneous fan-out | Defaults to `1_000` |
|
|
@@ -135,6 +135,26 @@ observes a provider but finds only a model-only rate, it raises
|
|
|
135
135
|
| `trace_normalizer` | Emit `:normalizer_decision` trace events from `Smith::Models::Normalizer` | Useful when debugging cross-provider request shaping |
|
|
136
136
|
| `ruby_llm_model_registry` | `:database` to require an AR-backed RubyLLM model registry; `:bundled` for the JSON fallback | Leave at default unless you've migrated to DB-backed |
|
|
137
137
|
|
|
138
|
+
### The `:provider_call` Trace
|
|
139
|
+
|
|
140
|
+
With `trace_provider_calls` on, every provider attempt emits one
|
|
141
|
+
`:provider_call` trace when it ends: `model`, `provider`, `attempt_id` (shared
|
|
142
|
+
with the attempt's usage entries), `attempt_index` (its position in the
|
|
143
|
+
fallback chain), `outcome` (`:success`, `:failure`, or `:aborted`), and
|
|
144
|
+
`duration_ms` once the timed call started. `agent_name` is the registered name
|
|
145
|
+
of the agent that made the attempt, so a generator and an evaluator sharing an
|
|
146
|
+
optimize round stay apart. `input_tokens` and `output_tokens` total the
|
|
147
|
+
provider-reported usage that the attempt's usage entries record (a completed
|
|
148
|
+
tool-loop prefix, and a failed attempt's own reported usage); both are omitted
|
|
149
|
+
when the provider reported none. A failed or aborted attempt adds `error_class`,
|
|
150
|
+
and `error_cause_class` for the error's direct cause (`Exception#cause`, else a
|
|
151
|
+
wrapping error's `wrapped_exception`, such as the `EOFError` inside a
|
|
152
|
+
`Faraday::ConnectionFailed`), omitted when there is none. Class names and token
|
|
153
|
+
counts are metadata, never content: `trace_content false` keeps them, messages
|
|
154
|
+
never appear, and a `trace_fields` allowlist for `:provider_call` must name
|
|
155
|
+
each field it wants. Ambient attribution (`transition`, `round`, and so on)
|
|
156
|
+
merges in as for every trace.
|
|
157
|
+
|
|
138
158
|
### Recommended First Additions
|
|
139
159
|
|
|
140
160
|
Add settings in this order:
|
data/docs/PATTERNS.md
CHANGED
|
@@ -377,6 +377,65 @@ class TranslationWorkflow < Smith::Workflow
|
|
|
377
377
|
end
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
`before_eval: ->(state, context) { ... }` runs after each candidate is
|
|
381
|
+
generated and before the evaluator is called, and may write to the workflow
|
|
382
|
+
context. When it returns a Hash whose `accept` (Symbol or String key) is
|
|
383
|
+
`false`, that Hash is the round's evaluation: Smith validates it exactly as
|
|
384
|
+
evaluator output (`feedback` is required, and a numeric `score` when
|
|
385
|
+
`improvement_threshold` is set), skips the evaluator for that round, and
|
|
386
|
+
continues as a rejection, so the feedback reaches the generator's refinement
|
|
387
|
+
turn and `on_exhaustion` applies when rounds run out. Convergence and the
|
|
388
|
+
improvement threshold read it as they read evaluator output. Any other return
|
|
389
|
+
value, including a Hash with `accept: true`, is ignored and the evaluator
|
|
390
|
+
judges as before. A deterministic check that makes a candidate unacceptable
|
|
391
|
+
can reject it this way without spending an evaluator call.
|
|
392
|
+
|
|
393
|
+
Every round's verdict is an output. Once its loop has run, the step's record
|
|
394
|
+
carries its verdicts under `:evaluations`, in attempt and round order, whether
|
|
395
|
+
the step completed or failed (a step that failed before its loop, or any other
|
|
396
|
+
step, has no such key), and `RunResult#evaluations` lists every optimize step's
|
|
397
|
+
verdicts with the step's transition:
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
result = TranslationWorkflow.new.run!
|
|
401
|
+
result.evaluations
|
|
402
|
+
# => [{ transition: :translate, attempt: 1, round: 0, source: :evaluator,
|
|
403
|
+
# verdict: { accept: false, feedback: "Keep the formal register.", score: 0.6 } },
|
|
404
|
+
# { transition: :translate, attempt: 1, round: 1, source: :evaluator,
|
|
405
|
+
# verdict: { accept: true, feedback: "ok", score: 0.93 } }]
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
`attempt` is the retry policy's attempt that ran the loop, from 1: when
|
|
409
|
+
`retry_on` runs the step again, the loop starts afresh and its rounds count
|
|
410
|
+
from 0 again, so an earlier attempt's verdicts, given and paid for all the
|
|
411
|
+
same, stay before it, and an attempt that failed before its loop leaves a gap.
|
|
412
|
+
`round` is the optimizer round, the one usage entries and traces carry.
|
|
413
|
+
`source` is `:evaluator`, or `:before_eval` for a rejection `before_eval`
|
|
414
|
+
returned. `verdict` is the evaluation as normalized (Symbol keys at every
|
|
415
|
+
depth), with every field the evaluator's schema declares, not only the
|
|
416
|
+
contract's. A verdict holds JSON values only (Hashes with String or Symbol
|
|
417
|
+
keys, Arrays, Strings, Symbols, Integers, finite Floats, true, false, nil), as
|
|
418
|
+
a provider's structured output does; anything else, most likely an object a
|
|
419
|
+
`before_eval` returned, fails the step with `Smith::WorkflowError`, the same
|
|
420
|
+
way in every run mode. So does a round whose verdict would take the step's
|
|
421
|
+
verdicts past the limits a split step's record is held to (depth, nodes and
|
|
422
|
+
bytes, `ExecutionResultSnapshot`'s), keeping the verdicts before it; a step's
|
|
423
|
+
output counts toward those limits only in a split step. The records on the
|
|
424
|
+
step and on `RunResult#evaluations` are frozen copies; a split step's
|
|
425
|
+
execution snapshot hands the host its own mutable copy of the whole step,
|
|
426
|
+
verdicts included. Only a valid verdict is recorded, and the one that ends the
|
|
427
|
+
loop is kept, whether it accepts, converges, or trips the improvement
|
|
428
|
+
threshold. A step whose completion fails after its loop (a split step's
|
|
429
|
+
snapshot over its limits) keeps its verdicts on the failure record. A nested
|
|
430
|
+
workflow's verdicts stay on its own run: the parent's step carries the child's
|
|
431
|
+
usage, not its verdicts. A callable
|
|
432
|
+
(`before_eval` or an exit mode) reads its own loop's verdicts so far from
|
|
433
|
+
`state.evaluations`, a frozen copy without `attempt`, so it cannot rewrite the
|
|
434
|
+
step's. Verdicts are content, so they are an output and never a
|
|
435
|
+
trace: nothing about them reaches a trace adapter. Like `steps`, they belong to
|
|
436
|
+
the run that executed the step, so a terminal restore has none; a host that
|
|
437
|
+
needs them later stores them when the run returns.
|
|
438
|
+
|
|
380
439
|
Why this matters:
|
|
381
440
|
|
|
382
441
|
- the loop is explicit, bounded, and observable
|
data/docs/PERSISTENCE.md
CHANGED
|
@@ -534,7 +534,11 @@ before writing when an open transaction has no exact identity capability.
|
|
|
534
534
|
If execution raises or the process dies after external work, the durable marker
|
|
535
535
|
remains set. A strict restore therefore fails closed with
|
|
536
536
|
`StepInProgressOnRestore`; the host must reconcile operation results or classify
|
|
537
|
-
the run as uncertain rather than blindly replaying the transition. The
|
|
537
|
+
the run as uncertain rather than blindly replaying the transition. The error's
|
|
538
|
+
`state` is the persisted state the interrupted step started from (a Symbol) and
|
|
539
|
+
its `transition` is the next transition the payload records, such as a routed
|
|
540
|
+
one, or nil when it records none, so the host can tell which step died without
|
|
541
|
+
reading Smith's payload itself. The same
|
|
538
542
|
in-memory workflow object cannot retry an attempted transition. If a host
|
|
539
543
|
transaction rolls back after a successful `persist!`,
|
|
540
544
|
`complete_persisted_step!` rejects the rolled-back checkpoint. When the adapter
|