smith-agents 0.7.0 → 0.10.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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +456 -0
  3. data/README.md +71 -1
  4. data/docs/CONFIGURATION.md +27 -6
  5. data/docs/PERSISTENCE.md +27 -1
  6. data/docs/TOOLS_AND_GUARDRAILS.md +233 -2
  7. data/lib/smith/agent/chat_construction.rb +112 -0
  8. data/lib/smith/agent/completion.rb +46 -0
  9. data/lib/smith/agent/completion_usage_recording.rb +43 -0
  10. data/lib/smith/agent/dynamic_configuration.rb +69 -0
  11. data/lib/smith/agent/fallback_configuration.rb +26 -0
  12. data/lib/smith/agent/invocation_preparation.rb +128 -0
  13. data/lib/smith/agent/lifecycle.rb +8 -275
  14. data/lib/smith/agent/model_reference.rb +76 -0
  15. data/lib/smith/agent/provider_attempt.rb +34 -0
  16. data/lib/smith/agent/provider_call_timing.rb +74 -0
  17. data/lib/smith/agent/provider_candidate_sequence.rb +55 -0
  18. data/lib/smith/agent/provider_completion.rb +148 -0
  19. data/lib/smith/agent/provider_failure_handling.rb +93 -0
  20. data/lib/smith/agent/provider_usage.rb +28 -0
  21. data/lib/smith/agent/reserved_input_bridge.rb +40 -0
  22. data/lib/smith/agent/usage_entry_recording.rb +66 -0
  23. data/lib/smith/agent/usage_traces.rb +49 -0
  24. data/lib/smith/agent/usage_tracking.rb +116 -0
  25. data/lib/smith/agent.rb +29 -197
  26. data/lib/smith/attribution.rb +115 -0
  27. data/lib/smith/context/observation_masking.rb +28 -4
  28. data/lib/smith/context/session.rb +7 -2
  29. data/lib/smith/diagnostic_text.rb +67 -0
  30. data/lib/smith/doctor/checks/configuration.rb +16 -3
  31. data/lib/smith/doctor/checks/models_registry.rb +75 -17
  32. data/lib/smith/errors.rb +34 -1
  33. data/lib/smith/event.rb +10 -2
  34. data/lib/smith/events/bus.rb +63 -5
  35. data/lib/smith/events/step_completed.rb +3 -0
  36. data/lib/smith/events/step_failed.rb +25 -0
  37. data/lib/smith/events/subscription.rb +8 -0
  38. data/lib/smith/models/ambiguous_profile_error.rb +9 -0
  39. data/lib/smith/models/collision_error.rb +9 -0
  40. data/lib/smith/models/inference.rb +14 -0
  41. data/lib/smith/models/normalizer.rb +11 -67
  42. data/lib/smith/models/provider_qualified_registry.rb +101 -0
  43. data/lib/smith/models/tool_routing.rb +140 -0
  44. data/lib/smith/models.rb +24 -78
  45. data/lib/smith/persisted_failure_invalid.rb +7 -0
  46. data/lib/smith/pricing.rb +138 -17
  47. data/lib/smith/pricing_configuration_error.rb +7 -0
  48. data/lib/smith/provider_permanent_failure.rb +16 -0
  49. data/lib/smith/tool/argument_container_reader.rb +97 -0
  50. data/lib/smith/tool/argument_scalar_snapshot.rb +99 -0
  51. data/lib/smith/tool/argument_snapshot.rb +19 -0
  52. data/lib/smith/tool/argument_snapshot_accounting.rb +69 -0
  53. data/lib/smith/tool/argument_snapshot_result.rb +15 -0
  54. data/lib/smith/tool/argument_snapshot_traversal.rb +119 -0
  55. data/lib/smith/tool/bounded_completion_context.rb +131 -0
  56. data/lib/smith/tool/bounded_completion_controls.rb +113 -0
  57. data/lib/smith/tool/bounded_completion_guard.rb +101 -0
  58. data/lib/smith/tool/bounded_completion_installation.rb +20 -0
  59. data/lib/smith/tool/bounded_completion_state.rb +57 -0
  60. data/lib/smith/tool/budget_enforcement.rb +15 -7
  61. data/lib/smith/tool/call_admission.rb +44 -0
  62. data/lib/smith/tool/call_allowance.rb +101 -28
  63. data/lib/smith/tool/call_allowance_counter.rb +43 -0
  64. data/lib/smith/tool/call_batch.rb +47 -0
  65. data/lib/smith/tool/call_budget.rb +51 -0
  66. data/lib/smith/tool/call_reservation.rb +42 -0
  67. data/lib/smith/tool/capture.rb +9 -1
  68. data/lib/smith/tool/chat_execution_callbacks.rb +58 -0
  69. data/lib/smith/tool/chat_execution_context.rb +7 -88
  70. data/lib/smith/tool/execution_authority.rb +42 -0
  71. data/lib/smith/tool/execution_authorization.rb +17 -0
  72. data/lib/smith/tool/execution_batch.rb +100 -0
  73. data/lib/smith/tool/execution_batch_admission.rb +56 -0
  74. data/lib/smith/tool/execution_batch_builder.rb +122 -0
  75. data/lib/smith/tool/execution_batch_collection.rb +70 -0
  76. data/lib/smith/tool/execution_batch_invocations.rb +28 -0
  77. data/lib/smith/tool/execution_batch_lifecycle.rb +91 -0
  78. data/lib/smith/tool/execution_batch_registry.rb +99 -0
  79. data/lib/smith/tool/execution_batch_requests.rb +59 -0
  80. data/lib/smith/tool/execution_batch_source_call.rb +36 -0
  81. data/lib/smith/tool/execution_batch_source_metadata.rb +107 -0
  82. data/lib/smith/tool/execution_batch_sources.rb +32 -0
  83. data/lib/smith/tool/execution_batch_state.rb +110 -0
  84. data/lib/smith/tool/execution_dispatch.rb +78 -0
  85. data/lib/smith/tool/execution_failure_handling.rb +101 -0
  86. data/lib/smith/tool/execution_lifecycle.rb +28 -0
  87. data/lib/smith/tool/execution_tracker.rb +20 -0
  88. data/lib/smith/tool/fail_fast_completion.rb +24 -0
  89. data/lib/smith/tool/graceful_completion.rb +20 -0
  90. data/lib/smith/tool/invocation.rb +27 -0
  91. data/lib/smith/tool/invocation_request.rb +37 -0
  92. data/lib/smith/tool/invocation_sequence.rb +26 -0
  93. data/lib/smith/tool/legacy_call_allowance.rb +57 -0
  94. data/lib/smith/tool/scoped_context.rb +60 -8
  95. data/lib/smith/tool.rb +81 -13
  96. data/lib/smith/tool_execution_not_admitted.rb +7 -0
  97. data/lib/smith/tool_failure_notification_failed.rb +94 -0
  98. data/lib/smith/trace/logger.rb +1 -0
  99. data/lib/smith/trace/memory.rb +40 -4
  100. data/lib/smith/trace/open_telemetry.rb +47 -3
  101. data/lib/smith/trace.rb +24 -5
  102. data/lib/smith/version.rb +2 -2
  103. data/lib/smith/workflow/agent_result.rb +4 -3
  104. data/lib/smith/workflow/artifact_integration.rb +2 -1
  105. data/lib/smith/workflow/composite/effects.rb +64 -31
  106. data/lib/smith/workflow/deadline_enforcement.rb +20 -2
  107. data/lib/smith/workflow/deterministic_execution.rb +2 -1
  108. data/lib/smith/workflow/deterministic_step.rb +14 -1
  109. data/lib/smith/workflow/dsl.rb +25 -1
  110. data/lib/smith/workflow/evaluator_optimizer.rb +3 -1
  111. data/lib/smith/workflow/event_integration.rb +48 -1
  112. data/lib/smith/workflow/execution.rb +10 -9
  113. data/lib/smith/workflow/failure_detail_snapshot.rb +33 -0
  114. data/lib/smith/workflow/failure_reconstructor.rb +79 -0
  115. data/lib/smith/workflow/failure_record.rb +139 -0
  116. data/lib/smith/workflow/failure_record_restore.rb +145 -0
  117. data/lib/smith/workflow/failure_record_text.rb +83 -0
  118. data/lib/smith/workflow/failure_record_validator.rb +113 -0
  119. data/lib/smith/workflow/fanout_execution.rb +1 -1
  120. data/lib/smith/workflow/guarded_step_execution.rb +39 -0
  121. data/lib/smith/workflow/guardrail_integration.rb +25 -0
  122. data/lib/smith/workflow/parallel.rb +14 -3
  123. data/lib/smith/workflow/parallel_execution.rb +6 -0
  124. data/lib/smith/workflow/persistence.rb +55 -20
  125. data/lib/smith/workflow/prepared_branch_execution.rb +13 -4
  126. data/lib/smith/workflow/retry_execution.rb +5 -1
  127. data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +9 -1
  128. data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +1 -0
  129. data/lib/smith/workflow/step_completion.rb +9 -0
  130. data/lib/smith/workflow/step_context.rb +46 -0
  131. data/lib/smith/workflow/thread_context_snapshot.rb +6 -2
  132. data/lib/smith/workflow/transition.rb +9 -4
  133. data/lib/smith/workflow/usage_entry.rb +43 -5
  134. data/lib/smith/workflow.rb +43 -131
  135. data/lib/smith.rb +10 -2
  136. metadata +82 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 450ce06c1d96f41a8b6ae6d0c0dd1b8dcc88b58d3da786d8536a26ecc8f111bb
4
- data.tar.gz: 7b0696ff9aaccc3016ebef5223c4ded71aeeb883d1a1b70e6f2ab5959657e1da
3
+ metadata.gz: 06665b52b660baf1f846ebd104caec677044595bd4aaf71ecf6ac3120e18d478
4
+ data.tar.gz: '09dd67e047e0f2a3dc6afd0f6b320ab7d5b7f2f23460ddae9576860e7b2e1329'
5
5
  SHA512:
6
- metadata.gz: da3c5cb55ef7efac304f223eb7ce468f058b4d85c9c4b60ea102f50882789d067eaf9c2a212871eb66a215f7f1cce2baa29de945dd3c0f2c0cc084d187fec1ae
7
- data.tar.gz: 910ec83bc644cdbb5ad1a737ef1aa95af3d5ca784d952fea9179ae4b7e44d49a1edf7a7ae2b067c7a83a227bb623d3d6c798cc45a8d254501ff397f99e876b69
6
+ metadata.gz: 76771bcb4d58d45f110b16a590579a34f698f1e9e2b75cca7806a5c5864f50c8209afc4a258a587069c6dd97f6ae87c1d0a62d434afd4b415f04e199561e79a6
7
+ data.tar.gz: 5160511ae311634881c774fa8040d3b69f93171e20e76aca595821c9eb7a767c76d0165ffe562190b16f624a4ba2e3e272822b3a1cf6b852ae2d2af7d7521994
data/CHANGELOG.md CHANGED
@@ -4,6 +4,462 @@ 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.10.0] - 2026-08-24
8
+
9
+ ### Added
10
+
11
+ - Last-serial-agent-execution attribution. A deterministic (`compute`) step can
12
+ now read the model and provider that actually served the most recent serial
13
+ `execute :agent` step via `DeterministicStep#last_agent_model` /
14
+ `#last_agent_provider` (symmetric with `#last_output`, which returns that
15
+ step's content). The value is the resolved model/provider after fallback
16
+ resolution, so a step that failed its primary and completed on a
17
+ `fallback_models` entry reports the model that actually ran, never the
18
+ configured primary. It is durable lifecycle state (persisted in `to_state` as
19
+ `last_agent_execution`, restored across crash/resume), not a trace scrape, and
20
+ is `nil` until a serial agent step runs. Backward compatible: pre-upgrade
21
+ states have no `last_agent_execution` key and restore to `nil`; old readers
22
+ slice off the unknown key.
23
+
24
+ ### Fixed
25
+
26
+ - Structured agent outputs recorded as session messages (Hash or Array
27
+ content) are now serialized to JSON at the RubyLLM provider boundary when
28
+ replayed to a later agent step. RubyLLM treats a Hash message content as
29
+ attachments and opens each value as a file, so replaying a prior structured
30
+ output to the next agent in a workflow session raised `Errno::ENOENT`. The
31
+ session store is unaffected: `last_output` and persisted `session_messages`
32
+ keep the raw structured value; only the provider-facing copy is serialized.
33
+
34
+ ## [0.9.0] - 2026-08-03
35
+
36
+ ### Upgrade notes
37
+
38
+ - Rollback after running fan-out branches under this version is a one-way
39
+ door for those runs: composite branch effects written with attribution
40
+ values (usage entries) or batch-correlated `tool_call_id` capture entries
41
+ fail an older gem's exact-key validation at reduction or recovery.
42
+ Drain in-flight composite runs before rolling the gem back; plain
43
+ checkpoint payloads are unaffected (restored pre-attribution documents
44
+ re-serialize byte-identically, and old readers slice off unknown keys).
45
+ - Hosts with a configured trace adapter see new output on upgrade without
46
+ any host change: one `:provider_call` line per provider attempt, one
47
+ `:cost` line per priced completed invocation, failed `:transition` lines
48
+ marked `outcome: :failed`, and ambient attribution keys (including
49
+ `execution_key`, the Smith persistence key) merged into every payload.
50
+ Identifier-only, but plan for the volume and shape change, especially
51
+ with `Smith::Trace::Logger` in production.
52
+ - Hosts with a `trace_fields` allowlist for `:transition` must add
53
+ `:outcome` (and the error keys they want) or failed transitions render
54
+ indistinguishable from successes under the allowlist.
55
+ - `Smith::Trace::Memory` is now bounded (default 10,000 entries, silent
56
+ drop with `dropped_count`); previously it accumulated without limit.
57
+
58
+ ### Added
59
+
60
+ - Add `Smith::Attribution`, an immutable thread-local execution attribution
61
+ context (`execution_key`, `transition`, `from`, `to`, `branch_key`,
62
+ `round`). Workflow execution installs it per step (the `execution_key`
63
+ defaults to the persistence key of a persisted run), fan-out carries it
64
+ into branch threads and overlays the branch key, and evaluator-optimizer
65
+ rounds overlay the round index. Hosts can seed an outer scope with
66
+ `Smith::Attribution.with(execution_key: ...)` around non-persisted runs.
67
+ Restoration inside workflow execution rides `ThreadContextSnapshot`, which
68
+ now tracks the attribution thread key. Scope overlays are nil-ignoring
69
+ (`Context#merge`), but the per-step facts (`transition`, `from`, `to`)
70
+ are replaced verbatim, nil included (`Context#override`): a nested
71
+ child's `from`-less transition never inherits the parent step's `from`,
72
+ and the failed `:transition` trace keeps `from`/`to` present even when
73
+ nil for the same reason.
74
+ - Merge ambient attribution fields into every `Smith::Trace.record` payload.
75
+ Attribution keys are identifiers, not content: caller-supplied keys win on
76
+ conflict, the content policy is unaffected, and a configured
77
+ `trace_fields` allowlist stays authoritative (add attribution keys to an
78
+ allowlist to receive them). Disable with `Smith.config.trace_attribution =
79
+ false` (default true).
80
+ - Bound `Smith::Trace::Memory` (default 10,000 entries) with a
81
+ `dropped_count` reader and a `snapshot` method for readers racing
82
+ concurrent recording.
83
+ - Tag `Workflow::UsageEntry` with the ambient attribution at recording time:
84
+ new optional members `transition`, `branch_key`, `round`, and `attempt_id`.
85
+ All four are nil on entries restored from checkpoints written by earlier
86
+ Smith versions and are omitted from serialization when nil, so restored
87
+ pre-attribution documents re-serialize byte-identically (hosts that digest
88
+ whole persisted documents in exact-mutation proofs depend on this). A
89
+ rolled-back gem drops the new keys from plain checkpoint payloads
90
+ (`from_h` slices to known members). Composite branch effects are the
91
+ exception: effects written by this version from a fan-out branch carry
92
+ real attribution values, and an older gem's exact-key effects validation
93
+ rejects them, so see the upgrade notes below before rolling back.
94
+ `recorded_at` now carries microsecond precision (`iso8601(6)`) on new
95
+ entries. Recording symbolizes `transition`/`branch_key` Strings exactly
96
+ as `from_h` does on restore, so a host seeding String attribution through
97
+ `Smith::Attribution.with` gets entries equal to their restored form.
98
+ - Measure each provider attempt with a monotonic clock around the whole chat
99
+ completion (including any provider tool loop) and emit one
100
+ `:provider_call` trace per attempt (success or failure) carrying `model`,
101
+ `provider`, `duration_ms`, `attempt_id`, `attempt_index`, and `outcome`.
102
+ Every usage entry the attempt produced shares its `attempt_id` (an attempt
103
+ with an N-round tool loop records N entries): join there for the attempt's
104
+ single duration, never sum across entries. Gate with
105
+ `Smith.config.trace_provider_calls` (default true). `ProviderAttempt`
106
+ gains optional `attempt_id` and `duration_ms`.
107
+ - Add a public read-only `Workflow#usage_entries` (frozen copy under the
108
+ recording mutex) so hosts can diff usage across a step boundary without a
109
+ full `to_state` serialization; `to_state` and `snapshot_usage_entries` now
110
+ read the ledger under the same mutex, so a state written mid-fan-out never
111
+ captures a torn array.
112
+ - Thread real correlation identity through events: `Smith::Event#execution_id`
113
+ and `#trace_id` default to the ambient attribution execution key (the
114
+ persistence key during persisted runs) instead of a fresh random UUID per
115
+ event; the random fallback remains for events built outside any execution
116
+ scope. The `:tool_call` trace gains a nullable `tool_call_id` when the
117
+ invocation came from a provider batch (the tool-results capture entry
118
+ gains the same key under a later bullet in this release; composite
119
+ effects accept the extended shape with bounded value validation).
120
+
121
+ - Emit from the step-failure paths, closing the success-only observation
122
+ gap: both `handle_step_failure` and the unresolved-transition handler now
123
+ record a `:transition` trace with `outcome: :failed` plus bounded
124
+ classification (`error_class`, `error_family` from FailureRecord's
125
+ taxonomy, `retryable`) and emit a new `Smith::Events::StepFailed` event.
126
+ Raw error messages never ride either; an emission failure is logged and
127
+ can never mask the original step error. The marker key is `outcome`
128
+ because `result` is a reserved content key in the trace pipeline. An
129
+ unresolved transition with no `:fail` transition still re-raises without
130
+ emitting: that path was never treated as a step. The unresolved handler
131
+ runs outside any step context, so it seeds the run identity explicitly;
132
+ both failure paths stamp persisted-run events with the persistence key.
133
+ Emission is terminal-per-step: a step that retries internally and then
134
+ succeeds emits only its `StepCompleted`; intermediate step-body retry
135
+ attempts stay dark at the step layer (provider-level failures remain
136
+ visible as `:provider_call` failure traces). `StepFailed` handlers run
137
+ outside the step snapshot's interrupt-masked region (emission is staged
138
+ in the failure rescue and flushed after the mask closes), interruptible
139
+ exactly like `StepCompleted` handlers. A step body that surfaces Smith's
140
+ own `UnresolvedTransitionError` emits exactly one `StepFailed` under the
141
+ real step identity; the unresolved handler recognizes the already-emitted
142
+ error instead of emitting a second event under the requested (never
143
+ executed) name.
144
+ - Emit the long-advertised `:cost` trace: one per completed agent
145
+ invocation, whose value is the sum of that invocation's per-response
146
+ usage-entry costs. Summing per response is what tiered catalogs actually
147
+ bill; pricing the aggregate token totals as one call would resolve the
148
+ wrong tier for multi-response tool loops. Emitted only for fully metered,
149
+ fully priced invocations (a partially priced or partially metered
150
+ invocation emits nothing rather than presenting an incomplete figure);
151
+ gated by the existing `trace_cost` setting (the per-type gates live in
152
+ the built-in adapters; a custom adapter receives every type). `:cost`
153
+ traces are not a spend total: billed failed and partial attempts appear
154
+ only in usage entries. The same per-response sum now becomes
155
+ `agent_result.cost`, so budget settlement, result surfaces, recorded
156
+ entries, and the trace all agree on one invocation cost.
157
+ - The OpenTelemetry adapter now creates retroactive spans with real
158
+ durations (span start backdated by `:tool_call` seconds or
159
+ `:provider_call` milliseconds; instant spans otherwise), preserves
160
+ numeric attribute types instead of stringifying everything, and uses only
161
+ the documented opentelemetry-api surface (`Tracer#start_span` with
162
+ `start_timestamp`, `Span#finish` with `end_timestamp`).
163
+
164
+ - Add a `workflow` discriminator to the ambient attribution, every trace
165
+ payload, usage entries, and the `StepCompleted`/`StepFailed` events: the
166
+ emitting workflow's class name ("anonymous" when unnamed), so
167
+ nested-child graph facts are distinguishable from parent facts under the
168
+ shared root execution identity. Nil-omitted from serialized entries like
169
+ the other attribution members.
170
+ - Every provider attempt now emits its `:provider_call` trace: `outcome` is
171
+ `:success`, `:failure` (provider failure, fallback may continue), or
172
+ `:aborted` (a non-provider error that re-raises), so prefix-accounted
173
+ usage entries always have their attempt join target.
174
+ - The tool-results capture entry gains `tool_call_id` when the invocation
175
+ came from a provider batch (omitted otherwise, so direct-invocation and
176
+ pre-existing payloads keep their exact two-key shape); composite effects
177
+ accept the extended shape while still rejecting unknown keys.
178
+ - `StepFailed` handlers now run outside the step snapshot's
179
+ interrupt-masked region: emission is staged in the failure rescue and
180
+ flushed after the mask closes with explicitly seeded run identity, so a
181
+ slow host handler can no longer make the workflow thread unkillable and
182
+ handlers match `StepCompleted`'s interruptibility.
183
+
184
+ ### Fixed
185
+
186
+ - A user-declared `:fail` transition no longer inherits the early order
187
+ position of the auto-generated placeholder created by `state :failed`:
188
+ redeclaring a generated transition takes a fresh declaration position, so
189
+ it can no longer shadow a same-origin primary transition at run time.
190
+ Genuine user redefinitions keep their original position, and subclasses
191
+ inherit the bookkeeping.
192
+ - Budget cost settlement now consumes the per-response priced sum instead
193
+ of pricing the invocation's aggregate token totals. Under linear pricing
194
+ the figures are identical; under tiered pricing the aggregate resolved
195
+ the wrong tier (or missed every tier and settled zero), so a
196
+ cost-budgeted workflow could keep spending after its real billed cost
197
+ exceeded the budget.
198
+
199
+ ### Removed
200
+
201
+ - Remove the never-read `trace_retention` and `trace_tenant_isolation`
202
+ settings. Both were silent no-ops since introduction; reading or writing
203
+ them now raises, so a host relying on the illusion fails loudly instead
204
+ of silently.
205
+
206
+ ### Changed
207
+
208
+ - Workflow step failure is now observable: subscribers to the events bus
209
+ receive `StepFailed` where previously failures emitted nothing (the
210
+ success-only scope is gone), and trace consumers see failed `:transition`
211
+ payloads distinguished by `outcome: :failed`.
212
+ - `Workflow::Composite::Effects` validates usage-entry keys as
213
+ required-plus-allowed instead of exact: entries from an older producer
214
+ (missing the optional attribution keys) stay valid, current entries with
215
+ attribution pass, and unknown keys still reject. The optional keys are
216
+ bounded values, not just bounded keys: `transition`, `branch_key`, and
217
+ `workflow` must be non-empty Strings up to 256 characters, `round` a
218
+ non-negative Integer, and `attempt_id` a UUID when present. Tool results
219
+ accept the extended capture shape: `tool_call_id`, when present, must be
220
+ a non-empty String up to 1024 characters (`tool` keeps its exact prior
221
+ validation).
222
+
223
+
224
+ - `Smith::Events` subscriptions now live in per-class buckets guarded by a
225
+ mutex: emit touches only the buckets for the event's ancestors instead of
226
+ scanning every subscription, dispatch order remains registration order,
227
+ and `is_a?` matching semantics are unchanged (instance-extended modules
228
+ dispatch through the singleton class; immediate values, which have no
229
+ singleton class and cannot be extended, dispatch through their class
230
+ ancestors). `Subscription#cancel` now
231
+ detaches from the registry, so cancelled subscriptions (including
232
+ `Events.within` scopes) no longer leak. Handlers run outside the registry
233
+ lock, so a handler may subscribe or cancel without deadlocking.
234
+ - `Smith::Trace::Memory#record` and `#clear!` are mutex-guarded and safe
235
+ under parallel fan-out branches.
236
+
237
+ ## [0.8.0] - 2026-07-25
238
+
239
+ ### Added
240
+
241
+ - Add opt-in graceful agent tool-budget exhaustion with a finite
242
+ `tool_calls` budget and `tool_budget_exhaustion :complete`. Smith consumes
243
+ model-requested calls atomically, rejects oversized batches without partial
244
+ execution, pairs every rejected call id, and performs one tool-disabled final
245
+ completion.
246
+ - Add one-use call admissions so an admitted Smith tool is not double charged,
247
+ while exact tool identity prevents nested tools from stealing an admission.
248
+ - Aggregate trustworthy token usage across every assistant response in a
249
+ successful RubyLLM tool loop, record one durable usage entry per provider
250
+ response, and preserve completed-prefix usage when a later provider round
251
+ fails.
252
+ - Expose immutable normalized tool-call metadata and one execution-scoped,
253
+ host-seedable invocation sequence across Smith-managed chats, branches,
254
+ threads, and fibers without introducing host persistence semantics.
255
+ - Add immutable exact tool-call budgets with aggregate and per-tool limits for
256
+ host-controlled execution scopes.
257
+ - Add a host-neutral whole-batch admission callback and per-invocation dispatch
258
+ failure callback. Smith snapshots immutable requests, admits the complete
259
+ Smith-managed subset of the provider batch before dispatch, and never reports
260
+ failure for a batch whose host admission callback raised.
261
+ - Add a Claude 5+ family inference rule: Fable, Mythos, Opus, Sonnet, and Haiku
262
+ ids with major version 5 or higher resolve to adaptive thinking, no
263
+ temperature accepted, and native tools-with-thinking, so an agent-declared
264
+ `temperature` is stripped instead of being sent to a provider that rejects
265
+ it. Dotted 4.x ids (for example `claude-haiku-4-5`) keep matching the 4.x
266
+ budget-tokens rule.
267
+ - Add new public error classes hosts may rescue or allowlist:
268
+ `Smith::PricingConfigurationError`, `Smith::ProviderPermanentFailure`
269
+ (carries `provider`, `model_id`, and `source_error_class`),
270
+ `Smith::Models::AmbiguousProfileError`, `Smith::Models::CollisionError`,
271
+ `Smith::ToolDispatchRejected`, `Smith::ToolOutcomeUncertain`,
272
+ `Smith::ToolExecutionNotAdmitted`, `Smith::ToolFailureNotificationFailed`,
273
+ `Smith::BoundedCompletionError`, and `Smith::PersistedFailureInvalid`.
274
+ Terminal tool-evidence families surface through
275
+ `Smith::Errors.retry_forbidden?` / `retry_forbidden_class?` and must never be
276
+ retried by host jobs.
277
+ - `Smith::Doctor` reports a dedicated failing `models.ambiguity` check when a
278
+ registered agent declares an unqualified model id registered under multiple
279
+ providers, instead of aborting the run with `AmbiguousProfileError`, and now
280
+ validates the assigned pricing catalog (legacy model-only keys, malformed
281
+ entries, and colliding keys fail the `config.pricing` check).
282
+ - Capture the causal failure classification behind an uncertain tool outcome
283
+ into the durable failure record: a persisted `Smith::ToolOutcomeUncertain`
284
+ carries bounded `error_cause_class`, `error_cause_family`, and
285
+ `error_cause_message` from its cause so a restored host can distinguish a
286
+ deadline, cancellation, or defect. The three attributes travel as one unit;
287
+ legacy records omit all three, and a partial or unknown-family set fails
288
+ closed at state admission.
289
+
290
+ ### Changed
291
+
292
+ - Bump `Smith::EXECUTION_SEMANTICS_VERSION` from `3` directly to `5` for exact
293
+ per-tool budgets, shared composite admission, bounded graceful completion,
294
+ and cumulative tool-loop usage. The value `4` was consumed transiently during
295
+ development of this slice and is intentionally skipped so a composite plan
296
+ persisted against an interim build can never read as compatible with the
297
+ released semantics; consumers compare by exact equality, so every pre-`5`
298
+ plan is invalidated either way.
299
+ - Key model profiles, fallback candidates, usage telemetry, and pricing by exact
300
+ provider/model identity. Provider-qualified pricing never falls back to a
301
+ legacy model-only rate, and fallback declarations must name their provider.
302
+ - Let observation masking preserve the exact immutable seed-message prefix
303
+ while bounding only later workflow observations. The prefix length is
304
+ persisted and validated across restart; legacy state defaults to zero.
305
+ - Require the exactly qualified RubyLLM `1.16.0`; graceful completion is
306
+ isolated behind that verified chat interface and fails closed on incompatible
307
+ hooks. Future RubyLLM versions require an explicit compatibility pass.
308
+ - Enforce the pricing-catalog key policy at admission time instead of inside
309
+ accounting: `Smith::Pricing.validate_catalog!` rejects legacy model-only
310
+ keys, unrecognized key shapes, malformed entries, and post-normalization
311
+ collisions, and the doctor runs it against the assigned catalog.
312
+ `Pricing.compute_cost` itself never raises: a provider-qualified lookup reads
313
+ only provider-qualified entries and returns nil (visibly unpriced) when only
314
+ a legacy rate exists, so an in-flight accounting path can never mask a
315
+ provider error with a pricing configuration error.
316
+ - Registering a model profile whose capabilities differ from the profile
317
+ already registered for the same provider/model identity now raises
318
+ `Smith::Models::CollisionError`; earlier releases silently replaced the
319
+ profile on Rails reload. Re-registering a value-identical profile stays
320
+ idempotent (see Migration notes).
321
+ - `Smith::Models::Normalizer` tool/endpoint routing no longer requires active
322
+ thinking: endpoint compatibility is evaluated on every chat construction, the
323
+ endpoint preserving the strictly larger compatible tool subset wins (ties
324
+ keep the current endpoint; a single forced tool that only one endpoint can
325
+ carry takes precedence), and each dropped tool records a `:tool_dropped`
326
+ decision. See docs/TOOLS_AND_GUARDRAILS.md.
327
+ - Agent tool evidence is tracked per transition: once any tool starts inside a
328
+ transition, a later provider failure in that transition (including one from a
329
+ concurrently executing parallel branch that never ran a tool itself) refuses
330
+ model fallback and surfaces `Smith::ToolOutcomeUncertain`. This is
331
+ deliberately conservative and fail-closed; branch-scoped evidence that
332
+ restores fallback for provably tool-free sibling branches is planned, and
333
+ child-workflow tool evidence does not yet mark the parent transition.
334
+ - `from_state` failure-record problems raise `Smith::PersistedFailureInvalid`
335
+ while other persisted-shape problems keep raising
336
+ `Smith::SerializationError`; both descend from `Smith::Error`, and hosts
337
+ rescuing `SerializationError` around restore must handle both.
338
+ - Restored failure records no longer resolve or construct arbitrary error
339
+ classes named by persisted data: host-defined subclasses reconstruct as their
340
+ Smith family parent (family `"other"` restores as `RuntimeError`), so
341
+ exact-class matching against restored `last_error` values must move to family
342
+ or `is_a?` checks.
343
+ - `Smith::Agent.fallback_models` now returns a frozen array of
344
+ `Smith::Agent::ModelReference` values (previously raw strings), block-form
345
+ `model {}` declarations must return a provider-qualified reference (a bare
346
+ string raises `Smith::AgentError`), permanent provider failures raise
347
+ `Smith::ProviderPermanentFailure` instead of a retryable `AgentError`,
348
+ non-provider `StandardError`s raised inside a provider attempt now propagate
349
+ raw instead of being wrapped, and `Workflow::UsageEntry` is frozen and gained
350
+ a `:provider` member.
351
+
352
+ ### Fixed
353
+
354
+ - Preserve the provider and model actually selected by RubyLLM in completion
355
+ usage, keep inherited fallback configuration immutable, and treat model-level
356
+ permission failures as model scoped so an eligible fallback on the same
357
+ provider remains available. Only account-wide authentication and payment
358
+ failures suppress later candidates from that provider.
359
+ - Prevent unavailable provider tool calls from creating an unbounded correction
360
+ loop by consuming the model-request allowance before dispatch.
361
+ - Prevent fallback-model restart after a Smith tool begins executing, because a
362
+ fresh chat would discard tool evidence and violate the provider protocol.
363
+ - Prevent transition retry, including broad explicit retry classes, when any
364
+ later provider, completion hook, output validation, or guardrail failure occurs
365
+ after a bound tool dispatch makes the external outcome uncertain.
366
+ - Reserve provider batches atomically against both the agent allowance and the
367
+ effective workflow tool budget, then reconcile unexecuted calls.
368
+ - Execute bounded tool loops iteratively so finite call allowances do not grow
369
+ Ruby stack depth, reject concurrent or callback reentry on one bounded chat,
370
+ and retain RubyLLM's native forced-tool-choice reset.
371
+ - Validate `Smith::Tool#perform` keyword arguments before budget charge and tool
372
+ execution, reject non-Smith bindings from the opt-in bounded policy, and fail
373
+ closed when raw provider params try to override reserved tool controls.
374
+ - Keep provider-facing tool controls and RubyLLM instrumentation aligned, and
375
+ restore tools, call preferences, and concurrency after success or failure.
376
+ - Permit bounded providers to return multiple calls in one response while more
377
+ than one call remains. Smith retains atomic batch reservation, deterministic
378
+ sequential execution, and fail-closed oversized-batch handling.
379
+ - Propagate one enclosing tool-call allowance into same-agent parallel and
380
+ heterogeneous fan-out worker threads so branches cannot multiply a host's
381
+ signed transition budget.
382
+ - Materialize one immutable admitted dispatch batch before host admission, so
383
+ later mutation of the provider collection, call identity, or arguments cannot
384
+ alter which Smith tools execute or the values they receive. The host callback
385
+ receives immutable source-call evidence while RubyLLM dispatches a separately
386
+ owned admitted call, and an atomic dispatch claim prevents callback re-entry
387
+ from executing the same admission twice. Continue to reject registered-tool
388
+ replacement and classify only guaranteed pre-`perform` failures as
389
+ `ToolDispatchRejected`.
390
+ - Capture provider batches with native `Hash` operations, bound the complete
391
+ provider batch to 100 calls and 1 MiB of UTF-8 call metadata, reject malformed
392
+ call protocol before host admission, and stop aggregate metadata capture as
393
+ soon as the bound is crossed.
394
+ - Bound immutable argument copying before child allocation, account for expanded
395
+ serialized occurrences even when a caller reuses a shared object graph,
396
+ admit each mutable container's native size before one shallow copy, verify the
397
+ copy retained that size before child allocation, reject hostile container
398
+ overrides and malformed UTF-8, and keep traversal iterative with linear time
399
+ and bounded space.
400
+ - Require one exact execution authority for every managed Smith tool dispatch.
401
+ Direct tool calls from provider callbacks and nested Smith tool calls now fail
402
+ closed; nested execution requires a future separately admitted primitive.
403
+ - Give terminal tool-evidence failures precedence during parallel arbitration
404
+ and centralize their non-retryable classification across declaration and
405
+ execution. Persist and restore terminal notification failures without resolving
406
+ or constructing arbitrary classes named by persisted data. Bound and normalize
407
+ failure diagnostics, capture failure class identity with native Ruby operations,
408
+ and reject inconsistent family/retry metadata or malformed typed failure details
409
+ at state admission.
410
+ - Cover the split execution files with direct-load contracts so each new file
411
+ loads standalone; the released artifact's file manifest is verified against
412
+ `lib/` at release time as part of the release procedure.
413
+ - Fail closed with a typed `Smith::AgentError` when an agent has no executable
414
+ model candidate (empty model chain), instead of leaking the exhausted
415
+ candidate sequence into nil destructuring from optimizer and orchestrator
416
+ paths.
417
+ - Attribute an account-wide authentication or payment failure to the attempted
418
+ reference's declared provider when the chat is unobservable, so a dead
419
+ provider account is not billed a second same-provider attempt.
420
+ - Deduplicate model candidates by physical identity, so a provider-unqualified
421
+ primary and a provider-qualified fallback naming the same model cannot
422
+ produce a duplicate attempt, and `ModelReference.coerce` now parses the
423
+ `"provider/model"` string form that `#to_s` emits (the first slash splits, so
424
+ slashed model ids round-trip).
425
+ - Keep captured and restored failure records symmetric for blank messages:
426
+ capture substitutes a deterministic placeholder for a blank error message, so
427
+ a workflow state whose last failed step had an empty message restores instead
428
+ of raising `Smith::PersistedFailureInvalid` and poisoning crash/resume.
429
+ - Restore legacy failure records whose `error_message` exceeds the 64 KiB
430
+ diagnostic bound by truncating exactly as capture truncates, instead of
431
+ rejecting the persisted workflow state for message length alone; non-text and
432
+ invalid-UTF-8 persisted values still fail closed.
433
+ - Scope an agent `tool_calls` budget under a legacy Hash tool-call allowance to
434
+ a typed rejection at both call sites instead of an untyped `NoMethodError`
435
+ mid-transition.
436
+ - Propagate a settled or exhausted batch reservation's refusal through
437
+ `CallAdmission#claim`, so a post-settlement execution can never run against
438
+ an already reconciled ledger.
439
+ - Keep a queued process-fatal sibling error ahead of a
440
+ `ToolFailureNotificationFailed` raised while notifying unsettled batch
441
+ failures, so notification problems cannot mask fatal arbitration outcomes.
442
+
443
+ ### Migration notes
444
+
445
+ - Pricing catalogs must move to provider-qualified keys (`%w[provider model]`
446
+ arrays or `"provider/model"` strings). Legacy model-only keys still price
447
+ provider-unqualified usage for compatibility, but they fail
448
+ `Smith::Pricing.validate_catalog!` (now run by the doctor), and
449
+ provider-qualified usage never reads them: such usage records nil cost until
450
+ the catalog is qualified.
451
+ - `fallback_models` entries must name their provider (`"provider/model"`, a
452
+ Hash, or a `ModelReference`); bare model ids fail closed at class definition.
453
+ - Hosts registering `Smith::Models` profiles from reloadable code must move
454
+ registration to boot-once initializers or restart after editing a profile;
455
+ value-different re-registration now raises `Smith::Models::CollisionError`.
456
+ - The gem now requires exactly `ruby_llm 1.16.0` (previously `>= 1.15,
457
+ < 1.17`); hosts on 1.15.x must upgrade together with this release.
458
+ - Hosts that rescued `Smith::AgentError` for permanent provider failures should
459
+ rescue `Smith::ProviderPermanentFailure`, and restore-time rescues of
460
+ `Smith::SerializationError` must also handle
461
+ `Smith::PersistedFailureInvalid`.
462
+
7
463
  ## [0.7.0] - 2026-07-21
8
464
 
9
465
  ### Added
data/README.md CHANGED
@@ -366,9 +366,41 @@ class SearchAgent < Smith::Agent
366
366
  register_as :search_agent
367
367
  model "claude-opus-4-7"
368
368
  tools Smith::Tools::WebSearch, Smith::Tools::UrlFetcher
369
+ budget tool_calls: 3
370
+ tool_budget_exhaustion :complete
369
371
  end
370
372
  ```
371
373
 
374
+ Agent tool-call budgets default to raising `Smith::BudgetExceeded` when a tool
375
+ tries to execute beyond the configured limit. Agents that declare
376
+ `tool_budget_exhaustion :complete` must also declare a finite `tool_calls`
377
+ budget. Smith then consumes each model-requested call before dispatch, executes
378
+ only a complete batch admitted against both agent and workflow limits, and
379
+ performs one tool-disabled completion when the allowance is exhausted or the
380
+ provider returns an oversized batch. The bounded provider loop is iterative,
381
+ not recursive. Calls to unavailable tools consume allowance as well, so a
382
+ provider cannot create an unbounded correction loop. Workflow reservations are
383
+ reconciled to actual Smith tool execution.
384
+
385
+ Hosts may install `Smith::Tool::CallBudget` with exact per-tool limits through
386
+ `Smith::Tool.with_call_budget`. Smith propagates that same root allowance into
387
+ parallel and heterogeneous fan-out worker threads; agent budgets narrow the
388
+ root and all branches share its atomic counters.
389
+
390
+ Graceful exhaustion is an in-process protocol guarantee, not durable recovery
391
+ for the model/tool loop. If the process exits after a tool begins and before the
392
+ agent completes, the host must treat the outcome as uncertain unless it owns a
393
+ durable per-invocation receipt and reconciliation contract. Smith also refuses
394
+ to start a fresh fallback-model chat after tool execution has begun because that
395
+ would discard the captured evidence and conversation protocol.
396
+
397
+ Within a host invocation scope, `Smith::Tool.current_invocation` exposes the
398
+ normalized tool-call ID and deterministic execution/batch ordinals. The ID is
399
+ RubyLLM correlation metadata and must not be treated as a provider idempotency
400
+ key. Durable hosts can pass an explicitly seeded
401
+ `Smith::Tool::InvocationSequence` to `with_invocation_context`; Smith itself does
402
+ not persist or reconcile operation receipts.
403
+
372
404
  Guardrails run as input/output gates around agent calls. See [`docs/TOOLS_AND_GUARDRAILS.md`](docs/TOOLS_AND_GUARDRAILS.md).
373
405
 
374
406
  ## Budgets and Deadlines
@@ -434,7 +466,25 @@ Smith ships a per-attempt normalizer that translates the request payload to what
434
466
  - OpenAI gpt-5 family reasoning_effort with `/v1/responses` routing when tools + thinking are combined
435
467
  - Gemini 2.5+ budget_tokens
436
468
 
437
- Override the inferred profile per-app via `Smith::Models.register(Profile.new(...))`. Hosts pin to specific model_ids by registering profiles; Smith never hardcodes model_ids in the library.
469
+ Override the inferred profile per-app via `Smith::Models.register(Profile.new(...))`.
470
+ Profiles are keyed by both provider and model id. Pass `provider:` to
471
+ `Smith::Models.find` or `find_or_infer` whenever an id exists under more than
472
+ one provider; an ambiguous unqualified lookup fails closed. Smith never
473
+ hardcodes model ids in the library.
474
+
475
+ Provider-qualified fallback entries use the same exact identity:
476
+
477
+ ```ruby
478
+ fallback_models(
479
+ { model: "claude-sonnet-4-6", provider: :anthropic },
480
+ { model: "openai/gpt-5", provider: :openrouter }
481
+ )
482
+ ```
483
+
484
+ Fallback entries must include an explicit provider. Smith does not infer a
485
+ fallback provider from the primary model or from a model id because the same id
486
+ may be registered by more than one provider. Unqualified fallback entries fail
487
+ closed during agent configuration.
438
488
 
439
489
  ## Errors and retry
440
490
 
@@ -446,6 +496,9 @@ Smith::Errors.retryable?(error)
446
496
 
447
497
  Smith::Errors.retryable_classes
448
498
  # => [Smith::AgentError, Smith::DeadlineExceeded] (for ActiveJob retry_on)
499
+
500
+ Smith::Errors.retry_forbidden?(error)
501
+ # => true for terminal tool-evidence failures that must never be replayed
449
502
  ```
450
503
 
451
504
  Workflow transitions can also declare a bounded local retry policy:
@@ -465,6 +518,23 @@ and external idempotency guarantees remain host-owned.
465
518
 
466
519
  - Executable graph reachability is iterative `O(V + E)` time and `O(V)` space,
467
520
  using indexed outgoing transitions and runtime-equivalent successor rules.
521
+ - Smith-managed tool arguments are copied iteratively in `O(N + B)` time and
522
+ `O(N + B)` bounded space for `N` expanded JSON value occurrences and `B`
523
+ expanded encoded bytes. Reused subgraphs are charged for every serialized
524
+ occurrence, native `Array` and `Hash` operations bypass hostile overrides, and
525
+ every mutable container's native size is admitted before one shallow copy is
526
+ made. Smith verifies the copy retained the admitted size before allocating or
527
+ traversing children. One immutable dispatch collection remains authoritative
528
+ from host admission through RubyLLM execution. The complete provider batch is
529
+ bounded to 100 calls and 1 MiB of normalized UTF-8 call metadata. The capture
530
+ boundary transfers temporary ownership of the provider collection and argument
531
+ graph to Smith: callers must not mutate them concurrently. Smith detects changed
532
+ cardinality and fails closed, but Ruby cannot prevent an unrelated writer from
533
+ growing a caller-owned container between native size admission and the native
534
+ copy without freezing that caller-owned object.
535
+ - Admitted tool-receipt settlement scans each batch once in `O(B)` time and
536
+ `O(B)` state for `B` admitted Smith calls, including when an earlier terminal
537
+ callback fails.
468
538
  - Parallel execution is `O(B)` scheduling and result space for `B` branches,
469
539
  with at most `C = parallel_concurrency` active worker threads per top-level
470
540
  execution. Re-entrant fan-out inherits the same cancellation signal and uses
@@ -49,6 +49,21 @@ Use workflow classes for orchestration behavior:
49
49
  - "How much total budget can the whole workflow consume?" -> workflow budget
50
50
  - "Which provider credentials should the app use?" -> RubyLLM, not Smith
51
51
 
52
+ ### Provider-qualified fallback models
53
+
54
+ Fallback models are exact model/provider identities:
55
+
56
+ ```ruby
57
+ fallback_models(
58
+ { model: "claude-sonnet-4-6", provider: :anthropic },
59
+ { model: "openai/gpt-5", provider: :openrouter }
60
+ )
61
+ ```
62
+
63
+ Every fallback must name its provider. Smith does not inherit the primary
64
+ model's provider or infer one from the model id; an unqualified fallback fails
65
+ closed during agent configuration.
66
+
52
67
  ### Full `Smith.configure` Example
53
68
 
54
69
  ```ruby
@@ -68,11 +83,11 @@ Smith.configure do |config|
68
83
  tool_call: %i[tool duration]
69
84
  }
70
85
  config.trace_content = false
71
- config.trace_retention = 86_400
72
- config.trace_tenant_isolation = false
86
+ config.trace_provider_calls = true
87
+ config.trace_attribution = true
73
88
 
74
89
  config.pricing = {
75
- "gpt-4.1-nano" => {
90
+ %w[openai gpt-4.1-nano] => {
76
91
  input_cost_per_token: 0.0000001,
77
92
  output_cost_per_token: 0.0000004
78
93
  }
@@ -82,6 +97,12 @@ Smith.configure do |config|
82
97
  end
83
98
  ```
84
99
 
100
+ Provider-qualified model calls require provider-qualified pricing keys. Smith
101
+ also accepts the string form `"openai/gpt-4.1-nano"`. Legacy model-only keys
102
+ remain available only for calls whose provider is genuinely unknown; if Smith
103
+ observes a provider but finds only a model-only rate, it raises
104
+ `Smith::PricingConfigurationError` instead of silently undercounting cost.
105
+
85
106
  ### What Each `Smith.configure` Setting Is For
86
107
 
87
108
  | Setting | What it controls | Typical first use |
@@ -94,11 +115,11 @@ end
94
115
  | `trace_transitions` | Emit transition traces | Usually leave on |
95
116
  | `trace_tool_calls` | Emit tool call traces | Usually leave on |
96
117
  | `trace_token_usage` | Emit usage traces | Useful for budget visibility |
97
- | `trace_cost` | Emit cost traces | Useful once pricing is configured |
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 |
98
120
  | `trace_fields` | Allowlist structural trace fields | Use when you want tighter trace output |
99
121
  | `trace_content` | Whether content appears in traces | Leave `false` first |
100
- | `trace_retention` | Trace retention policy hook | Useful when traces leave memory |
101
- | `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 |
102
123
  | `pricing` | Best-known model-call cost catalog | Add once you care about `total_cost` |
103
124
  | `logger` | Smith's runtime logger | Usually the first setting to add |
104
125
  | `persistence_adapter` | Adapter for durable workflow state | `:redis`, `:rails_cache`, `:active_record`, `:memory`, or a custom object |