smith-agents 0.7.0 → 0.8.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +226 -0
  3. data/README.md +71 -1
  4. data/docs/CONFIGURATION.md +22 -1
  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 +25 -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 +104 -0
  13. data/lib/smith/agent/lifecycle.rb +7 -276
  14. data/lib/smith/agent/model_reference.rb +76 -0
  15. data/lib/smith/agent/provider_attempt.rb +28 -0
  16. data/lib/smith/agent/provider_candidate_sequence.rb +55 -0
  17. data/lib/smith/agent/provider_completion.rb +130 -0
  18. data/lib/smith/agent/provider_failure_handling.rb +93 -0
  19. data/lib/smith/agent/provider_usage.rb +28 -0
  20. data/lib/smith/agent/reserved_input_bridge.rb +40 -0
  21. data/lib/smith/agent/usage_entry_recording.rb +43 -0
  22. data/lib/smith/agent/usage_tracking.rb +113 -0
  23. data/lib/smith/agent.rb +29 -197
  24. data/lib/smith/context/observation_masking.rb +28 -4
  25. data/lib/smith/context/session.rb +7 -2
  26. data/lib/smith/diagnostic_text.rb +67 -0
  27. data/lib/smith/doctor/checks/configuration.rb +16 -3
  28. data/lib/smith/doctor/checks/models_registry.rb +75 -17
  29. data/lib/smith/errors.rb +34 -1
  30. data/lib/smith/models/ambiguous_profile_error.rb +9 -0
  31. data/lib/smith/models/collision_error.rb +9 -0
  32. data/lib/smith/models/inference.rb +14 -0
  33. data/lib/smith/models/normalizer.rb +11 -67
  34. data/lib/smith/models/provider_qualified_registry.rb +101 -0
  35. data/lib/smith/models/tool_routing.rb +140 -0
  36. data/lib/smith/models.rb +24 -78
  37. data/lib/smith/persisted_failure_invalid.rb +7 -0
  38. data/lib/smith/pricing.rb +138 -17
  39. data/lib/smith/pricing_configuration_error.rb +7 -0
  40. data/lib/smith/provider_permanent_failure.rb +16 -0
  41. data/lib/smith/tool/argument_container_reader.rb +97 -0
  42. data/lib/smith/tool/argument_scalar_snapshot.rb +99 -0
  43. data/lib/smith/tool/argument_snapshot.rb +19 -0
  44. data/lib/smith/tool/argument_snapshot_accounting.rb +69 -0
  45. data/lib/smith/tool/argument_snapshot_result.rb +15 -0
  46. data/lib/smith/tool/argument_snapshot_traversal.rb +119 -0
  47. data/lib/smith/tool/bounded_completion_context.rb +131 -0
  48. data/lib/smith/tool/bounded_completion_controls.rb +113 -0
  49. data/lib/smith/tool/bounded_completion_guard.rb +101 -0
  50. data/lib/smith/tool/bounded_completion_installation.rb +20 -0
  51. data/lib/smith/tool/bounded_completion_state.rb +57 -0
  52. data/lib/smith/tool/budget_enforcement.rb +15 -7
  53. data/lib/smith/tool/call_admission.rb +44 -0
  54. data/lib/smith/tool/call_allowance.rb +101 -28
  55. data/lib/smith/tool/call_allowance_counter.rb +43 -0
  56. data/lib/smith/tool/call_batch.rb +47 -0
  57. data/lib/smith/tool/call_budget.rb +51 -0
  58. data/lib/smith/tool/call_reservation.rb +42 -0
  59. data/lib/smith/tool/chat_execution_callbacks.rb +58 -0
  60. data/lib/smith/tool/chat_execution_context.rb +7 -88
  61. data/lib/smith/tool/execution_authority.rb +42 -0
  62. data/lib/smith/tool/execution_authorization.rb +17 -0
  63. data/lib/smith/tool/execution_batch.rb +100 -0
  64. data/lib/smith/tool/execution_batch_admission.rb +56 -0
  65. data/lib/smith/tool/execution_batch_builder.rb +122 -0
  66. data/lib/smith/tool/execution_batch_collection.rb +70 -0
  67. data/lib/smith/tool/execution_batch_invocations.rb +28 -0
  68. data/lib/smith/tool/execution_batch_lifecycle.rb +91 -0
  69. data/lib/smith/tool/execution_batch_registry.rb +99 -0
  70. data/lib/smith/tool/execution_batch_requests.rb +59 -0
  71. data/lib/smith/tool/execution_batch_source_call.rb +36 -0
  72. data/lib/smith/tool/execution_batch_source_metadata.rb +107 -0
  73. data/lib/smith/tool/execution_batch_sources.rb +32 -0
  74. data/lib/smith/tool/execution_batch_state.rb +110 -0
  75. data/lib/smith/tool/execution_dispatch.rb +78 -0
  76. data/lib/smith/tool/execution_failure_handling.rb +101 -0
  77. data/lib/smith/tool/execution_lifecycle.rb +28 -0
  78. data/lib/smith/tool/execution_tracker.rb +20 -0
  79. data/lib/smith/tool/fail_fast_completion.rb +24 -0
  80. data/lib/smith/tool/graceful_completion.rb +20 -0
  81. data/lib/smith/tool/invocation.rb +27 -0
  82. data/lib/smith/tool/invocation_request.rb +37 -0
  83. data/lib/smith/tool/invocation_sequence.rb +26 -0
  84. data/lib/smith/tool/legacy_call_allowance.rb +57 -0
  85. data/lib/smith/tool/scoped_context.rb +60 -8
  86. data/lib/smith/tool.rb +71 -12
  87. data/lib/smith/tool_execution_not_admitted.rb +7 -0
  88. data/lib/smith/tool_failure_notification_failed.rb +94 -0
  89. data/lib/smith/version.rb +2 -2
  90. data/lib/smith/workflow/agent_result.rb +4 -3
  91. data/lib/smith/workflow/artifact_integration.rb +2 -1
  92. data/lib/smith/workflow/composite/effects.rb +3 -1
  93. data/lib/smith/workflow/deadline_enforcement.rb +20 -2
  94. data/lib/smith/workflow/execution.rb +2 -9
  95. data/lib/smith/workflow/failure_detail_snapshot.rb +33 -0
  96. data/lib/smith/workflow/failure_reconstructor.rb +79 -0
  97. data/lib/smith/workflow/failure_record.rb +139 -0
  98. data/lib/smith/workflow/failure_record_restore.rb +145 -0
  99. data/lib/smith/workflow/failure_record_text.rb +83 -0
  100. data/lib/smith/workflow/failure_record_validator.rb +113 -0
  101. data/lib/smith/workflow/guarded_step_execution.rb +39 -0
  102. data/lib/smith/workflow/parallel.rb +14 -3
  103. data/lib/smith/workflow/persistence.rb +22 -19
  104. data/lib/smith/workflow/retry_execution.rb +5 -1
  105. data/lib/smith/workflow/thread_context_snapshot.rb +5 -2
  106. data/lib/smith/workflow/transition.rb +9 -4
  107. data/lib/smith/workflow/usage_entry.rb +12 -2
  108. data/lib/smith/workflow.rb +17 -130
  109. data/lib/smith.rb +3 -0
  110. metadata +78 -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: 1fe948e343baa2573448f7a95a964729e96a71b0b2e6ef16226694400ba0fb97
4
+ data.tar.gz: 8f3d35e4ee6998ca41a4be0e19dcce0b5e843b44c0f28167aff3d5326d1be573
5
5
  SHA512:
6
- metadata.gz: da3c5cb55ef7efac304f223eb7ce468f058b4d85c9c4b60ea102f50882789d067eaf9c2a212871eb66a215f7f1cce2baa29de945dd3c0f2c0cc084d187fec1ae
7
- data.tar.gz: 910ec83bc644cdbb5ad1a737ef1aa95af3d5ca784d952fea9179ae4b7e44d49a1edf7a7ae2b067c7a83a227bb623d3d6c798cc45a8d254501ff397f99e876b69
6
+ metadata.gz: 85bc7336ef0cb273979c4c7df1a4c0c724eb74cb34c4b49c1b23768e9454c700a1f33827ac9a1b33ff9680efd55b302b282db0fc553c5e20c52b4d13970044fa
7
+ data.tar.gz: afe1281c6ef7967292c1c43b7370ca5ab3795abcc98722aa8e10c03266347dcd2411d9c44878f1c0d4aa41df5f88ffd636d46084ff996f2008f60baa650ce5fc
data/CHANGELOG.md CHANGED
@@ -4,6 +4,232 @@ 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.8.0] - 2026-07-25
8
+
9
+ ### Added
10
+
11
+ - Add opt-in graceful agent tool-budget exhaustion with a finite
12
+ `tool_calls` budget and `tool_budget_exhaustion :complete`. Smith consumes
13
+ model-requested calls atomically, rejects oversized batches without partial
14
+ execution, pairs every rejected call id, and performs one tool-disabled final
15
+ completion.
16
+ - Add one-use call admissions so an admitted Smith tool is not double charged,
17
+ while exact tool identity prevents nested tools from stealing an admission.
18
+ - Aggregate trustworthy token usage across every assistant response in a
19
+ successful RubyLLM tool loop, record one durable usage entry per provider
20
+ response, and preserve completed-prefix usage when a later provider round
21
+ fails.
22
+ - Expose immutable normalized tool-call metadata and one execution-scoped,
23
+ host-seedable invocation sequence across Smith-managed chats, branches,
24
+ threads, and fibers without introducing host persistence semantics.
25
+ - Add immutable exact tool-call budgets with aggregate and per-tool limits for
26
+ host-controlled execution scopes.
27
+ - Add a host-neutral whole-batch admission callback and per-invocation dispatch
28
+ failure callback. Smith snapshots immutable requests, admits the complete
29
+ Smith-managed subset of the provider batch before dispatch, and never reports
30
+ failure for a batch whose host admission callback raised.
31
+ - Add a Claude 5+ family inference rule: Fable, Mythos, Opus, Sonnet, and Haiku
32
+ ids with major version 5 or higher resolve to adaptive thinking, no
33
+ temperature accepted, and native tools-with-thinking, so an agent-declared
34
+ `temperature` is stripped instead of being sent to a provider that rejects
35
+ it. Dotted 4.x ids (for example `claude-haiku-4-5`) keep matching the 4.x
36
+ budget-tokens rule.
37
+ - Add new public error classes hosts may rescue or allowlist:
38
+ `Smith::PricingConfigurationError`, `Smith::ProviderPermanentFailure`
39
+ (carries `provider`, `model_id`, and `source_error_class`),
40
+ `Smith::Models::AmbiguousProfileError`, `Smith::Models::CollisionError`,
41
+ `Smith::ToolDispatchRejected`, `Smith::ToolOutcomeUncertain`,
42
+ `Smith::ToolExecutionNotAdmitted`, `Smith::ToolFailureNotificationFailed`,
43
+ `Smith::BoundedCompletionError`, and `Smith::PersistedFailureInvalid`.
44
+ Terminal tool-evidence families surface through
45
+ `Smith::Errors.retry_forbidden?` / `retry_forbidden_class?` and must never be
46
+ retried by host jobs.
47
+ - `Smith::Doctor` reports a dedicated failing `models.ambiguity` check when a
48
+ registered agent declares an unqualified model id registered under multiple
49
+ providers, instead of aborting the run with `AmbiguousProfileError`, and now
50
+ validates the assigned pricing catalog (legacy model-only keys, malformed
51
+ entries, and colliding keys fail the `config.pricing` check).
52
+ - Capture the causal failure classification behind an uncertain tool outcome
53
+ into the durable failure record: a persisted `Smith::ToolOutcomeUncertain`
54
+ carries bounded `error_cause_class`, `error_cause_family`, and
55
+ `error_cause_message` from its cause so a restored host can distinguish a
56
+ deadline, cancellation, or defect. The three attributes travel as one unit;
57
+ legacy records omit all three, and a partial or unknown-family set fails
58
+ closed at state admission.
59
+
60
+ ### Changed
61
+
62
+ - Bump `Smith::EXECUTION_SEMANTICS_VERSION` from `3` directly to `5` for exact
63
+ per-tool budgets, shared composite admission, bounded graceful completion,
64
+ and cumulative tool-loop usage. The value `4` was consumed transiently during
65
+ development of this slice and is intentionally skipped so a composite plan
66
+ persisted against an interim build can never read as compatible with the
67
+ released semantics; consumers compare by exact equality, so every pre-`5`
68
+ plan is invalidated either way.
69
+ - Key model profiles, fallback candidates, usage telemetry, and pricing by exact
70
+ provider/model identity. Provider-qualified pricing never falls back to a
71
+ legacy model-only rate, and fallback declarations must name their provider.
72
+ - Let observation masking preserve the exact immutable seed-message prefix
73
+ while bounding only later workflow observations. The prefix length is
74
+ persisted and validated across restart; legacy state defaults to zero.
75
+ - Require the exactly qualified RubyLLM `1.16.0`; graceful completion is
76
+ isolated behind that verified chat interface and fails closed on incompatible
77
+ hooks. Future RubyLLM versions require an explicit compatibility pass.
78
+ - Enforce the pricing-catalog key policy at admission time instead of inside
79
+ accounting: `Smith::Pricing.validate_catalog!` rejects legacy model-only
80
+ keys, unrecognized key shapes, malformed entries, and post-normalization
81
+ collisions, and the doctor runs it against the assigned catalog.
82
+ `Pricing.compute_cost` itself never raises: a provider-qualified lookup reads
83
+ only provider-qualified entries and returns nil (visibly unpriced) when only
84
+ a legacy rate exists, so an in-flight accounting path can never mask a
85
+ provider error with a pricing configuration error.
86
+ - Registering a model profile whose capabilities differ from the profile
87
+ already registered for the same provider/model identity now raises
88
+ `Smith::Models::CollisionError`; earlier releases silently replaced the
89
+ profile on Rails reload. Re-registering a value-identical profile stays
90
+ idempotent (see Migration notes).
91
+ - `Smith::Models::Normalizer` tool/endpoint routing no longer requires active
92
+ thinking: endpoint compatibility is evaluated on every chat construction, the
93
+ endpoint preserving the strictly larger compatible tool subset wins (ties
94
+ keep the current endpoint; a single forced tool that only one endpoint can
95
+ carry takes precedence), and each dropped tool records a `:tool_dropped`
96
+ decision. See docs/TOOLS_AND_GUARDRAILS.md.
97
+ - Agent tool evidence is tracked per transition: once any tool starts inside a
98
+ transition, a later provider failure in that transition (including one from a
99
+ concurrently executing parallel branch that never ran a tool itself) refuses
100
+ model fallback and surfaces `Smith::ToolOutcomeUncertain`. This is
101
+ deliberately conservative and fail-closed; branch-scoped evidence that
102
+ restores fallback for provably tool-free sibling branches is planned, and
103
+ child-workflow tool evidence does not yet mark the parent transition.
104
+ - `from_state` failure-record problems raise `Smith::PersistedFailureInvalid`
105
+ while other persisted-shape problems keep raising
106
+ `Smith::SerializationError`; both descend from `Smith::Error`, and hosts
107
+ rescuing `SerializationError` around restore must handle both.
108
+ - Restored failure records no longer resolve or construct arbitrary error
109
+ classes named by persisted data: host-defined subclasses reconstruct as their
110
+ Smith family parent (family `"other"` restores as `RuntimeError`), so
111
+ exact-class matching against restored `last_error` values must move to family
112
+ or `is_a?` checks.
113
+ - `Smith::Agent.fallback_models` now returns a frozen array of
114
+ `Smith::Agent::ModelReference` values (previously raw strings), block-form
115
+ `model {}` declarations must return a provider-qualified reference (a bare
116
+ string raises `Smith::AgentError`), permanent provider failures raise
117
+ `Smith::ProviderPermanentFailure` instead of a retryable `AgentError`,
118
+ non-provider `StandardError`s raised inside a provider attempt now propagate
119
+ raw instead of being wrapped, and `Workflow::UsageEntry` is frozen and gained
120
+ a `:provider` member.
121
+
122
+ ### Fixed
123
+
124
+ - Preserve the provider and model actually selected by RubyLLM in completion
125
+ usage, keep inherited fallback configuration immutable, and treat model-level
126
+ permission failures as model scoped so an eligible fallback on the same
127
+ provider remains available. Only account-wide authentication and payment
128
+ failures suppress later candidates from that provider.
129
+ - Prevent unavailable provider tool calls from creating an unbounded correction
130
+ loop by consuming the model-request allowance before dispatch.
131
+ - Prevent fallback-model restart after a Smith tool begins executing, because a
132
+ fresh chat would discard tool evidence and violate the provider protocol.
133
+ - Prevent transition retry, including broad explicit retry classes, when any
134
+ later provider, completion hook, output validation, or guardrail failure occurs
135
+ after a bound tool dispatch makes the external outcome uncertain.
136
+ - Reserve provider batches atomically against both the agent allowance and the
137
+ effective workflow tool budget, then reconcile unexecuted calls.
138
+ - Execute bounded tool loops iteratively so finite call allowances do not grow
139
+ Ruby stack depth, reject concurrent or callback reentry on one bounded chat,
140
+ and retain RubyLLM's native forced-tool-choice reset.
141
+ - Validate `Smith::Tool#perform` keyword arguments before budget charge and tool
142
+ execution, reject non-Smith bindings from the opt-in bounded policy, and fail
143
+ closed when raw provider params try to override reserved tool controls.
144
+ - Keep provider-facing tool controls and RubyLLM instrumentation aligned, and
145
+ restore tools, call preferences, and concurrency after success or failure.
146
+ - Permit bounded providers to return multiple calls in one response while more
147
+ than one call remains. Smith retains atomic batch reservation, deterministic
148
+ sequential execution, and fail-closed oversized-batch handling.
149
+ - Propagate one enclosing tool-call allowance into same-agent parallel and
150
+ heterogeneous fan-out worker threads so branches cannot multiply a host's
151
+ signed transition budget.
152
+ - Materialize one immutable admitted dispatch batch before host admission, so
153
+ later mutation of the provider collection, call identity, or arguments cannot
154
+ alter which Smith tools execute or the values they receive. The host callback
155
+ receives immutable source-call evidence while RubyLLM dispatches a separately
156
+ owned admitted call, and an atomic dispatch claim prevents callback re-entry
157
+ from executing the same admission twice. Continue to reject registered-tool
158
+ replacement and classify only guaranteed pre-`perform` failures as
159
+ `ToolDispatchRejected`.
160
+ - Capture provider batches with native `Hash` operations, bound the complete
161
+ provider batch to 100 calls and 1 MiB of UTF-8 call metadata, reject malformed
162
+ call protocol before host admission, and stop aggregate metadata capture as
163
+ soon as the bound is crossed.
164
+ - Bound immutable argument copying before child allocation, account for expanded
165
+ serialized occurrences even when a caller reuses a shared object graph,
166
+ admit each mutable container's native size before one shallow copy, verify the
167
+ copy retained that size before child allocation, reject hostile container
168
+ overrides and malformed UTF-8, and keep traversal iterative with linear time
169
+ and bounded space.
170
+ - Require one exact execution authority for every managed Smith tool dispatch.
171
+ Direct tool calls from provider callbacks and nested Smith tool calls now fail
172
+ closed; nested execution requires a future separately admitted primitive.
173
+ - Give terminal tool-evidence failures precedence during parallel arbitration
174
+ and centralize their non-retryable classification across declaration and
175
+ execution. Persist and restore terminal notification failures without resolving
176
+ or constructing arbitrary classes named by persisted data. Bound and normalize
177
+ failure diagnostics, capture failure class identity with native Ruby operations,
178
+ and reject inconsistent family/retry metadata or malformed typed failure details
179
+ at state admission.
180
+ - Cover the split execution files with direct-load contracts so each new file
181
+ loads standalone; the released artifact's file manifest is verified against
182
+ `lib/` at release time as part of the release procedure.
183
+ - Fail closed with a typed `Smith::AgentError` when an agent has no executable
184
+ model candidate (empty model chain), instead of leaking the exhausted
185
+ candidate sequence into nil destructuring from optimizer and orchestrator
186
+ paths.
187
+ - Attribute an account-wide authentication or payment failure to the attempted
188
+ reference's declared provider when the chat is unobservable, so a dead
189
+ provider account is not billed a second same-provider attempt.
190
+ - Deduplicate model candidates by physical identity, so a provider-unqualified
191
+ primary and a provider-qualified fallback naming the same model cannot
192
+ produce a duplicate attempt, and `ModelReference.coerce` now parses the
193
+ `"provider/model"` string form that `#to_s` emits (the first slash splits, so
194
+ slashed model ids round-trip).
195
+ - Keep captured and restored failure records symmetric for blank messages:
196
+ capture substitutes a deterministic placeholder for a blank error message, so
197
+ a workflow state whose last failed step had an empty message restores instead
198
+ of raising `Smith::PersistedFailureInvalid` and poisoning crash/resume.
199
+ - Restore legacy failure records whose `error_message` exceeds the 64 KiB
200
+ diagnostic bound by truncating exactly as capture truncates, instead of
201
+ rejecting the persisted workflow state for message length alone; non-text and
202
+ invalid-UTF-8 persisted values still fail closed.
203
+ - Scope an agent `tool_calls` budget under a legacy Hash tool-call allowance to
204
+ a typed rejection at both call sites instead of an untyped `NoMethodError`
205
+ mid-transition.
206
+ - Propagate a settled or exhausted batch reservation's refusal through
207
+ `CallAdmission#claim`, so a post-settlement execution can never run against
208
+ an already reconciled ledger.
209
+ - Keep a queued process-fatal sibling error ahead of a
210
+ `ToolFailureNotificationFailed` raised while notifying unsettled batch
211
+ failures, so notification problems cannot mask fatal arbitration outcomes.
212
+
213
+ ### Migration notes
214
+
215
+ - Pricing catalogs must move to provider-qualified keys (`%w[provider model]`
216
+ arrays or `"provider/model"` strings). Legacy model-only keys still price
217
+ provider-unqualified usage for compatibility, but they fail
218
+ `Smith::Pricing.validate_catalog!` (now run by the doctor), and
219
+ provider-qualified usage never reads them: such usage records nil cost until
220
+ the catalog is qualified.
221
+ - `fallback_models` entries must name their provider (`"provider/model"`, a
222
+ Hash, or a `ModelReference`); bare model ids fail closed at class definition.
223
+ - Hosts registering `Smith::Models` profiles from reloadable code must move
224
+ registration to boot-once initializers or restart after editing a profile;
225
+ value-different re-registration now raises `Smith::Models::CollisionError`.
226
+ - The gem now requires exactly `ruby_llm 1.16.0` (previously `>= 1.15,
227
+ < 1.17`); hosts on 1.15.x must upgrade together with this release.
228
+ - Hosts that rescued `Smith::AgentError` for permanent provider failures should
229
+ rescue `Smith::ProviderPermanentFailure`, and restore-time rescues of
230
+ `Smith::SerializationError` must also handle
231
+ `Smith::PersistedFailureInvalid`.
232
+
7
233
  ## [0.7.0] - 2026-07-21
8
234
 
9
235
  ### 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
@@ -72,7 +87,7 @@ Smith.configure do |config|
72
87
  config.trace_tenant_isolation = false
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 |
data/docs/PERSISTENCE.md CHANGED
@@ -12,7 +12,7 @@ Example:
12
12
  class ReviewContext < Smith::Context
13
13
  persist :ticket_id, :current_findings, :source_urls
14
14
 
15
- session_strategy :observation_masking, window: 6
15
+ session_strategy :observation_masking, window: 6, preserve_seed: true
16
16
 
17
17
  inject_state do |persisted|
18
18
  <<~TEXT
@@ -37,12 +37,22 @@ end
37
37
  What Smith does for you:
38
38
 
39
39
  - prepares masked session input at step boundaries
40
+ - optionally preserves the complete immutable `seed_messages` prefix, with its
41
+ original roles and order, while bounding only later workflow observations
40
42
  - injects a state summary message into that prepared input
41
43
  - persists declared workflow context keys
42
44
  - persists accepted session history
43
45
  - preserves chosen next transitions across persistence
44
46
  - supports JSON host round-trips through `to_state` and `.from_state`
45
47
 
48
+ `preserve_seed: true` is intended for workflows whose seed is durable external
49
+ conversation and whose later accepted outputs are internal stage handoffs. Smith
50
+ persists and validates the exact seed-prefix length, so restart does not infer
51
+ the boundary from message content. The masking pass is linear in session length
52
+ and retains only the seed prefix, injected system messages, and the configured
53
+ observation tail. Legacy state without `seed_message_count` restores with no
54
+ preserved prefix.
55
+
46
56
  Example host-controlled persistence:
47
57
 
48
58
  ```ruby
@@ -305,6 +315,22 @@ and JSON serialization hooks. It is bound by object identity to one workflow
305
315
  instance and by process id to the issuing Ruby process, so a forked child cannot
306
316
  consume inherited authority. It is not a lease, cross-process fence, or durable
307
317
  attempt receipt.
318
+
319
+ Persisted workflow failures use an explicit reconstruction registry. Smith does
320
+ not resolve arbitrary class names from persisted data or invoke their
321
+ constructors. Known retry-forbidden families retain their non-retryable
322
+ semantics even when the original class is no longer available; unknown classes
323
+ restore as `RuntimeError`. Failure messages and details are normalized into
324
+ bounded JSON-safe evidence before persistence; invalid or oversized details are
325
+ represented by an explicit omission marker. Inconsistent persisted class,
326
+ family, retry-policy, or known typed-detail evidence fails closed during
327
+ `from_state` as `Smith::PersistedFailureInvalid`. Failure capture uses native
328
+ Ruby class and message operations so an exception cannot override diagnostic
329
+ methods to suppress its durable terminal record. Terminal
330
+ `Smith::ToolFailureNotificationFailed` records preserve bounded diagnostic
331
+ details for both the dispatch failure and the failed host notification and
332
+ restore as the same non-retryable failure family.
333
+
308
334
  Before consuming the capability, a host may inspect the exact captured agent
309
335
  bindings with a block:
310
336