smith-agents 0.4.1 → 0.4.3

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +91 -3
  3. data/README.md +69 -3
  4. data/UPSTREAM_PROPOSAL.md +19 -0
  5. data/docs/PATTERNS.md +85 -6
  6. data/lib/smith/agent/lifecycle.rb +11 -7
  7. data/lib/smith/agent/registry/introspection.rb +27 -0
  8. data/lib/smith/agent/registry.rb +22 -17
  9. data/lib/smith/agent/registry_binding.rb +43 -0
  10. data/lib/smith/agent.rb +3 -4
  11. data/lib/smith/doctor/checks/models_registry.rb +25 -14
  12. data/lib/smith/doctor/checks/persistence_capabilities.rb +37 -29
  13. data/lib/smith/version.rb +1 -1
  14. data/lib/smith/workflow/agent_result.rb +26 -0
  15. data/lib/smith/workflow/branch_env.rb +24 -0
  16. data/lib/smith/workflow/budget_integration.rb +27 -5
  17. data/lib/smith/workflow/deterministic_execution.rb +1 -1
  18. data/lib/smith/workflow/deterministic_step.rb +32 -4
  19. data/lib/smith/workflow/durability.rb +38 -12
  20. data/lib/smith/workflow/evaluator_optimizer.rb +18 -10
  21. data/lib/smith/workflow/event_integration.rb +3 -3
  22. data/lib/smith/workflow/execution.rb +8 -1
  23. data/lib/smith/workflow/fanout_execution.rb +121 -0
  24. data/lib/smith/workflow/graph/contract_helpers.rb +65 -0
  25. data/lib/smith/workflow/graph/fanout_contract.rb +140 -0
  26. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +98 -0
  27. data/lib/smith/workflow/graph/optimization_contract.rb +96 -0
  28. data/lib/smith/workflow/graph/orchestration_contract.rb +83 -0
  29. data/lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb +124 -0
  30. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +114 -0
  31. data/lib/smith/workflow/graph/runtime_readiness.rb +63 -0
  32. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +87 -0
  33. data/lib/smith/workflow/graph/runtime_readiness_report.rb +65 -0
  34. data/lib/smith/workflow/graph/targets.rb +5 -0
  35. data/lib/smith/workflow/graph/transition_diagnostics.rb +7 -0
  36. data/lib/smith/workflow/graph/transition_snapshot.rb +80 -18
  37. data/lib/smith/workflow/graph.rb +16 -1
  38. data/lib/smith/workflow/graph_dsl.rb +4 -0
  39. data/lib/smith/workflow/guardrail_integration.rb +45 -10
  40. data/lib/smith/workflow/nested_execution.rb +5 -4
  41. data/lib/smith/workflow/optimization_state.rb +13 -0
  42. data/lib/smith/workflow/orchestration_state.rb +13 -0
  43. data/lib/smith/workflow/orchestrator_worker.rb +5 -16
  44. data/lib/smith/workflow/parallel/cancellation.rb +9 -0
  45. data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
  46. data/lib/smith/workflow/parallel.rb +12 -16
  47. data/lib/smith/workflow/parallel_execution.rb +5 -1
  48. data/lib/smith/workflow/persistence.rb +37 -6
  49. data/lib/smith/workflow/retry_execution.rb +52 -0
  50. data/lib/smith/workflow/router.rb +15 -4
  51. data/lib/smith/workflow/run_result.rb +54 -0
  52. data/lib/smith/workflow/transition.rb +260 -24
  53. data/lib/smith/workflow/usage_entry.rb +30 -0
  54. data/lib/smith/workflow/worker_execution.rb +13 -0
  55. data/lib/smith/workflow.rb +40 -136
  56. data/lib/smith.rb +12 -0
  57. metadata +24 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fce22e9b87caf5c01417a5e7d8fe7a6f4ee179abebc61644074400452a078791
4
- data.tar.gz: e7d2ae59746ff545f5d45215bcec20dea95ee82a37d7fa765c4ded95484d4029
3
+ metadata.gz: 8b8a1ad3fc2cb40ef0287063f81e0bc01a6bca349b1625c4173daef763785202
4
+ data.tar.gz: b870358edea9686d15e164bb3d44e00edf8e50da8cb2d68b8a6a666bbc5d0f79
5
5
  SHA512:
6
- metadata.gz: 8d8515c459487f57c20301581e6f1d5cc786f8bfe7089b4e628548145ac56e0c9c93e9560ac37f1fa7818f95a71a1c8753cb275267b341fb39ca4bf63d5e5f3e
7
- data.tar.gz: b3e05ca16fc7fec62e8172d9d6ccb84b0bc3f7546af22b9f47c8a36ce4b7e6e5bfb35b0fb47eb51c0b86cf4ab4fbe47e651406c1666fcc63d4b2c16b0ccb0bf9
6
+ metadata.gz: bd9dd710fb5008efc7896e950b52621d3c80dab9be902b323efdc76c3d4d12e4b1c066b2219e5e36c56edea5652add7406ecb1d740db79b320035eee9047cee6
7
+ data.tar.gz: 4d5e6b412d39269276096b697a399ad476dd1d4bc370792d8acfdd8f49041b567da2120e56a0fc1e090cbf4c351c9849201c8b0030fd5518f6c69495b175f534
data/CHANGELOG.md CHANGED
@@ -6,7 +6,95 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- No unreleased changes.
9
+ ## [0.4.3] - 2026-07-05
10
+
11
+ ### Documentation
12
+
13
+ - Clarify Smith's repair and wait-style loop boundaries: `retry_on` and
14
+ `optimize` are executable today, deterministic repair and guarded re-entry
15
+ are not native first-class contracts yet, and durable polling/wait semantics
16
+ remain host-owned unless an explicit wait contract exists.
17
+
18
+ ### Added
19
+
20
+ - Static graph-inspection contracts for `optimize` and `orchestrate`
21
+ transitions, including bounded loop/delegation settings, schema labels, output
22
+ contracts, exit policies, dispatch semantics, and transition-level resume
23
+ guarantees.
24
+ - `Workflow.runtime_readiness`, a static diagnostic report that separates graph
25
+ topology validity from runtime binding readiness without executing agents,
26
+ tools, providers, jobs, or persistence.
27
+ - Runtime-readiness diagnostics for unresolved, invalid, lazy/uninspectable,
28
+ model-less, and model-required agent bindings across execute, route,
29
+ optimize, orchestrate, nested, and fan-out workflow shapes.
30
+ - Runtime-readiness metrics now expose direct counts and transitive counts folded
31
+ in from nested workflows.
32
+ - `Smith::Agent::Registry.binding_for` and `.bindings` expose non-resolving
33
+ registry inspection for diagnostics and host cleanup.
34
+ - Richer fan-out transition snapshot metadata: branch count, join state,
35
+ ordered branch list, output contract, resume contract, and per-branch result
36
+ contracts for named branch-result output.
37
+ - Direct doctor coverage for registered agent model-profile checks, including
38
+ static primary and static fallback models, making safe-default model shaping
39
+ explicit before hosts rely on runtime behavior.
40
+
41
+ ### Changed
42
+
43
+ - `smith doctor --profile rails_persistence` now reports the full optional
44
+ persistence capability surface (`store_versioned`, `record_heartbeat`, and
45
+ `last_heartbeat`) instead of checking optimistic locking only.
46
+ - Workflow runtime value objects now live in dedicated files while preserving
47
+ the existing public constants (`Smith::Workflow::RunResult`,
48
+ `AgentResult`, `UsageEntry`, `BranchEnv`, and internal execution helpers).
49
+ - Release documentation now reflects the current heartbeat optional-capability
50
+ contract and the RubyLLM integration boundary.
51
+
52
+ ### Test coverage
53
+
54
+ - Default suite: 926 examples, 0 failures.
55
+ - Practical gem-level execution probe covering 30 varied workflows across
56
+ strict/lax idempotency, same-agent parallel branches, heterogeneous fan-out,
57
+ retry metadata, optimizer contracts, and orchestrator-worker flows.
58
+ - Smith Studio host verification against the local Smith checkout: 186 runtime
59
+ tests and a 30-scenario generated-class lifecycle proof gate.
60
+ - Built `pkg/smith-agents-0.4.3.gem` and smoke-tested `require "smith"` from
61
+ the unpacked package.
62
+
63
+ ## [0.4.2] - 2026-07-02
64
+
65
+ Patch release for bounded fan-out and retry workflow primitives. This remains
66
+ workflow-first and host-owned: Smith executes declared transitions and exposes
67
+ inspection metadata, while durable scheduling, long waits, tool adapter
68
+ contracts, and deployment packaging stay with the host application.
69
+
70
+ ### Added
71
+
72
+ - `fan_out branches: {...}` transition DSL for bounded heterogeneous
73
+ multi-agent fan-out with stable branch keys and named aggregate results.
74
+ - `retry_on` transition DSL for bounded local retries using explicit error
75
+ classes or Smith's built-in retryability classifier.
76
+ - Graph inspection metadata for `:fanout` transitions and retry policy details.
77
+
78
+ ### Changed
79
+
80
+ - Fan-out branch execution preserves branch identity, branch-specific budgets,
81
+ agent guardrails, tool guardrails, deadlines, and usage accounting.
82
+ - Parallel/fan-out failure handling now prefers the initiating branch error over
83
+ cooperative cancellation errors.
84
+ - Failed-but-billable provider attempts are included in budget reconciliation
85
+ for retry, fallback, and fan-out settlement paths.
86
+ - Retry `max_delay` remains a hard cap even when jitter is configured.
87
+
88
+ ### Test coverage
89
+
90
+ - Default suite: 880 examples, 0 failures.
91
+ - Practical gem-level execution probe covering heterogeneous `fan_out`,
92
+ same-agent parallel execution, `retry_on`, failed-but-billable budget
93
+ settlement, cancellation cause preservation, branch input guardrail ordering
94
+ before session preparation, graph metadata, and invalid declaration rejection.
95
+ - Added focused coverage for heterogeneous fan-out, retry policies,
96
+ failed-but-billable retry budget accounting, cancellation cause preservation,
97
+ and graph inspection metadata.
10
98
 
11
99
  ## [0.4.1] - 2026-06-28
12
100
 
@@ -92,7 +180,7 @@ This release tracks two thematic refactors that together harden the agent-invoca
92
180
  - `Smith::Models` registry (Dry::Container-backed) for application-side `Smith::Models::Profile` overrides; mirrors the `Smith::Agent::Registry` stale-reload-binding pattern for Rails autoreload safety.
93
181
  - `Smith::Models::Profile` immutable capability record (`Data.define`) covering thinking_shape, accepts_temperature, tools_with_thinking_native, tools_with_thinking_route, and a derived `endpoint_mode`.
94
182
  - `Smith::Models::Inference` library-shipped pattern rules describing each provider family's payload shape (Anthropic Opus 4.7+ adaptive, Anthropic 4.0-4.6 budget_tokens, OpenAI gpt-5 family reasoning_effort + responses route, OpenAI gpt-4.x, Gemini 2.5+ budget_tokens, etc.). Smith ships zero hardcoded model_ids; new model releases that match an existing pattern work without library changes.
95
- - `Smith::Models::Normalizer.apply!(chat, profile:)` per-attempt request shaper. Translates Anthropic Opus 4.7+ thinking to the adaptive payload shape (`@params[:thinking] = { type: "adaptive" }` + `output_config[:effort]`), nulls temperature where the resolved profile forbids it, routes `(gpt-5 + tools + thinking)` via `openai_api_mode: :responses` when supported, drops incompatible tools otherwise. Hooks at `Smith::Agent.chat()` so direct callers (e.g., InvokeCleaner-style usage outside the workflow lifecycle) are normalized too.
183
+ - `Smith::Models::Normalizer.apply!(chat, profile:)` per-attempt request shaper. Translates Anthropic Opus 4.7+ thinking to the adaptive payload shape (`@params[:thinking] = { type: "adaptive" }` + `output_config[:effort]`), nulls temperature where the resolved profile forbids it, routes `(gpt-5 + tools + thinking)` via `openai_api_mode: :responses` when supported, drops incompatible tools otherwise. Hooks at `Smith::Agent.chat()` so direct callers outside the workflow lifecycle are normalized too.
96
184
  - `Smith::Agent::RESERVED_INPUT_NAMES = %i[model_id provider endpoint_mode]` auto-injected into `runtime_context` per attempt from the resolved profile. The `Smith::Agent.inputs` getter returns reserved ∪ user (frozen, deduplicated); the setter raises `Smith::AgentError` if a user-declared name collides with a reserved name.
97
185
  - `Smith::Tool.compatible_with(...)` DSL for declaring per-(provider, endpoint) tool compatibility. `Smith::Tool.inherited` dups the spec so subclasses inherit the parent's compatibility metadata.
98
186
  - `Smith::Tools::Think` declares `compatible_with :anthropic, :gemini, openai: :responses`. Drops gracefully on OpenAI chat-completions when `openai_api_mode = :off`, runs via `/v1/responses` when `:auto`.
@@ -152,4 +240,4 @@ This release tracks two thematic refactors that together harden the agent-invoca
152
240
 
153
241
  ## [0.1.0] - Initial public-track release
154
242
 
155
- Initial pre-release tagged for the Hadithi consumer's local-path dependency. No formal changelog prior to the Phase A/B refactor.
243
+ Initial pre-release. No formal changelog prior to the Phase A/B refactor.
data/README.md CHANGED
@@ -5,11 +5,19 @@ Workflow-first multi-agent orchestration for Ruby. Smith sits on top of `RubyLLM
5
5
  > [!WARNING]
6
6
  > Smith is pre-1.0. Expect contract tightening between minor versions. Pin to an exact version in production.
7
7
 
8
+ ## Verification Discipline
9
+
10
+ Tests are required, but they are never enough for runtime primitive changes.
11
+ Every Smith workflow slice must also run practical gem-level execution probes.
12
+ When a host application consumes unreleased Smith changes, point that host app at
13
+ the local Smith repository and exercise the changed workflow paths in the host
14
+ environment before calling the slice complete.
15
+
8
16
  ## Installation
9
17
 
10
18
  ```ruby
11
19
  # Gemfile
12
- gem "smith-agents", "~> 0.2.0", require: "smith"
20
+ gem "smith-agents", "~> 0.4.3", require: "smith"
13
21
  ```
14
22
 
15
23
  ```bash
@@ -87,6 +95,7 @@ end
87
95
  | Pipeline | sequential transitions | Multi-step workflow with explicit success/failure routing. |
88
96
  | Router | `route :classifier, routes: {...}` | Branch on a classifier agent's output. |
89
97
  | Parallel fan-out | `execute :agent, parallel: true` | Concurrent agent calls under one ledger. |
98
+ | Heterogeneous fan-out | `fan_out branches: {...}` | Concurrent calls to different agents with named branch results. |
90
99
  | Nested workflow | `workflow OtherWorkflow` | Reuse a subflow as one transition. |
91
100
  | Evaluator-Optimizer | `optimize generator:, evaluator:, ...` | Generate-then-critique refinement loops. |
92
101
  | Orchestrator-Worker | `orchestrate orchestrator:, worker:, ...` | Dynamic task fan-out with delegation rounds. |
@@ -94,6 +103,21 @@ end
94
103
 
95
104
  The full pattern guide with working examples for each lives in [`docs/PATTERNS.md`](docs/PATTERNS.md).
96
105
 
106
+ ### Repair And Wait Boundaries
107
+
108
+ Smith only owns repair and wait-style loop behavior when the bounds and stop
109
+ conditions are explicit and enforceable inside the workflow step. Durable
110
+ timers, queue delivery, and wake-up policy remain host-owned.
111
+ For bounded dynamic delegation, use the separate Orchestrator-Worker pattern.
112
+
113
+ | Contract | Status | Smith mapping |
114
+ |---|---|---|
115
+ | Retry loop | Executable | `retry_on`, bounded to one transition. |
116
+ | Evaluator-Optimizer | Executable | `optimize`, bounded by `max_rounds` plus structured evaluator output. |
117
+ | Deterministic repair | Not first-class yet | Can be handwritten with `compute` / `run` only when the workflow author owns the exact guard, repair, revalidation, and exit policy. Deterministic steps may declare inspectable `routes: [...]`, but that is not a native repair-loop contract. |
118
+ | Guarded state re-entry | Not first-class yet | `compute` / `run` can declare and route to named transitions with `routes: [...]`, but Smith does not yet own persisted entry counts, mutation policy, or safe re-entry contracts. |
119
+ | Polling / wait | Host-owned | Use the host app's queue/timer plus Smith persistence helpers. Smith must not model durable polling with busy-waits or sleep loops. |
120
+
97
121
  ## Workflow Graph Inspection
98
122
 
99
123
  Smith can inspect a workflow's declared graph without running agents or advancing state. This is useful for host apps that want to render, lint, or cache a workflow shape before execution.
@@ -109,6 +133,31 @@ report.metrics # => state, transition, reachability, and terminal-state co
109
133
 
110
134
  Graph inspection is static and diagnostic-only. Runtime execution, persistence, progress projection, retries, and recovery remain host-owned concerns.
111
135
 
136
+ Smith also exposes a static runtime-readiness report for checks that require
137
+ declared runtime bindings but still do not execute the workflow:
138
+
139
+ ```ruby
140
+ readiness = ReplyWorkflow.runtime_readiness
141
+
142
+ readiness.ready? # => true when no readiness/topology errors exist
143
+ readiness.status # => :ready, :warning, or :not_ready
144
+ readiness.diagnostics # => topology diagnostics + runtime binding diagnostics
145
+ ```
146
+
147
+ Runtime readiness checks graph topology, registered agent bindings, model
148
+ requirements for structured runtime roles, lazy/uninspectable bindings, invalid
149
+ non-agent bindings, nested workflow readiness, and fan-out branch binding counts.
150
+ It does not call providers, resolve lazy container blocks, run tools, enqueue
151
+ jobs, or verify host-owned durability.
152
+
153
+ Readiness metrics include both direct graph counts and transitive counts folded
154
+ in from nested workflows.
155
+
156
+ Transition snapshots include runtime contracts for complex primitives where
157
+ Smith owns executable semantics: heterogeneous fan-out, evaluator-optimizer, and
158
+ orchestrator-worker transitions expose bounded settings, output shapes, and
159
+ transition-level resume behavior for host renderers and compilers.
160
+
112
161
  ## Configuration
113
162
 
114
163
  ```ruby
@@ -153,7 +202,10 @@ result = ReplyWorkflow.run_persisted!(
153
202
  )
154
203
  ```
155
204
 
156
- Built-in adapters (all support TTL where the backend allows; `Redis`, `ActiveRecord`, `Memory` also support optimistic locking via `store_versioned`):
205
+ Built-in adapters (all support TTL where the backend allows; `Redis`,
206
+ `ActiveRecord`, and `Memory` support optimistic locking via `store_versioned`;
207
+ `Redis` and `Memory` also support heartbeat probes via `record_heartbeat` /
208
+ `last_heartbeat`):
157
209
 
158
210
  - `:memory` — in-process Hash, intended for tests and `test_mode = true`
159
211
  - `:redis` — Redis client; uses WATCH/MULTI/EXEC for CAS
@@ -230,6 +282,19 @@ Smith::Errors.retryable_classes
230
282
  # => [Smith::AgentError, Smith::DeadlineExceeded] (for ActiveJob retry_on)
231
283
  ```
232
284
 
285
+ Workflow transitions can also declare a bounded local retry policy:
286
+
287
+ ```ruby
288
+ transition :draft, from: :idle, to: :done do
289
+ execute :writer
290
+ retry_on Smith::AgentError, attempts: 3, backoff: 0.1, max_delay: 1.0
291
+ end
292
+ ```
293
+
294
+ When no classes are passed, `retry_on` uses `Smith::Errors.retryable?`.
295
+ This is a bounded local transition retry policy. Durable scheduling, long waits,
296
+ and external idempotency guarantees remain host-owned.
297
+
233
298
  ## Development
234
299
 
235
300
  ```bash
@@ -238,4 +303,5 @@ bundle exec rspec
238
303
  bundle exec rubocop
239
304
  ```
240
305
 
241
- 770 examples, MIT licensed. See [`CHANGELOG.md`](CHANGELOG.md) for the 0.2.0 surface and [`UPSTREAM_PROPOSAL.md`](UPSTREAM_PROPOSAL.md) for the vendored Responses adapter retirement path.
306
+ 880 examples, MIT licensed. See [`CHANGELOG.md`](CHANGELOG.md) for the current
307
+ release surface.
data/UPSTREAM_PROPOSAL.md CHANGED
@@ -139,3 +139,22 @@ What stays Smith-owned (orchestration concerns, not provider-API concerns):
139
139
 
140
140
  - RubyLLM PR #770 (OpenAI `/v1/responses` support) is the related upstream track. Smith's vendored `Smith::Providers::OpenAI::Responses` retires when #770 merges.
141
141
  - This proposal (capability profiles + before_complete) is a separate, additive RubyLLM RFC. Once accepted, Smith files a migration PR to consume it.
142
+
143
+ ## Current RubyLLM Docs Check
144
+
145
+ As of the release-prep audit, RubyLLM's official documentation describes a model
146
+ registry with model capability and pricing data, Rails DB-backed model registry
147
+ support, provider overrides, instrumentation, and concurrent tool execution.
148
+ Smith should continue treating RubyLLM as the source of truth for provider
149
+ communication and model inventory while keeping Smith-specific workflow
150
+ semantics, request shaping, and tool-compatibility policy explicit.
151
+
152
+ Release implication: before deleting Smith's model normalizer, Responses
153
+ adapter, or model-profile inference layer, verify that RubyLLM exposes the
154
+ specific public hooks Smith needs for:
155
+
156
+ - clearing incompatible temperature/thinking settings without private ivar
157
+ mutation
158
+ - request-shaping hooks before provider payload rendering
159
+ - endpoint selection for tools plus thinking/reasoning combinations
160
+ - model capability fields that distinguish Smith's workflow-relevant cases
data/docs/PATTERNS.md CHANGED
@@ -239,7 +239,49 @@ Why this is valuable:
239
239
  - branch failures discard step output and route through normal failure handling
240
240
  - prepared input is reused consistently across branches
241
241
 
242
- ## Example 6: Nested Workflows
242
+ ## Example 6: Heterogeneous Fan-Out
243
+
244
+ Use heterogeneous fan-out when different specialists should run concurrently and return named branch results under one workflow transition.
245
+
246
+ ```ruby
247
+ class StaticReviewAgent < Smith::Agent
248
+ register_as :static_review_agent
249
+ model "gpt-4.1-nano"
250
+ end
251
+
252
+ class SecurityReviewAgent < Smith::Agent
253
+ register_as :security_review_agent
254
+ model "gpt-4.1-nano"
255
+ end
256
+
257
+ class CodeReviewWorkflow < Smith::Workflow
258
+ initial_state :idle
259
+ state :reviewed
260
+ state :failed
261
+
262
+ transition :review, from: :idle, to: :reviewed do
263
+ fan_out branches: {
264
+ static: :static_review_agent,
265
+ security: :security_review_agent
266
+ }
267
+ on_failure :fail
268
+ end
269
+ end
270
+ ```
271
+
272
+ What you get:
273
+
274
+ - stable branch identity in the step output
275
+ - branch-specific agent budgets, guardrails, tools, and model configuration
276
+ - one shared prepared input for the transition
277
+ - one shared transition result, so downstream joins remain explicit in the workflow
278
+ - branch failures discard partial output and route through normal failure handling
279
+ - graph inspection exposes the join state, branch count, ordered branch map,
280
+ output contract, resume contract, and per-branch named result contracts
281
+
282
+ Use same-agent `parallel: true` for repeated homogeneous work. Use `fan_out` when branches are different agents with different responsibilities.
283
+
284
+ ## Example 7: Nested Workflows
243
285
 
244
286
  Use nested workflows when one part of the system deserves to be a reusable subflow with its own states and transitions.
245
287
 
@@ -281,7 +323,7 @@ What you get:
281
323
  - nested best-known token/cost totals roll up into the parent result
282
324
  - artifact scope is preserved across nesting
283
325
 
284
- ## Example 7: Evaluator-Optimizer
326
+ ## Example 8: Evaluator-Optimizer
285
327
 
286
328
  Use `optimize` when one agent generates candidates and another agent evaluates whether the result is acceptable.
287
329
 
@@ -334,8 +376,42 @@ Why this matters:
334
376
  - acceptance criteria are structured
335
377
  - exhaustion, malformed evaluator output, and convergence without acceptance fail normally
336
378
  - costs and token usage from the full loop roll into the workflow totals
337
-
338
- ## Example 8: Orchestrator-Worker
379
+ - graph inspection exposes the generator/evaluator bindings, max rounds, schema
380
+ label, evaluator context, exit modes, required/conditional evaluator output
381
+ fields, and transition-level resume contract
382
+
383
+ ## Repair And Wait Boundaries
384
+
385
+ Smith's workflow layer is intentionally bounded. A repair or wait-style loop is
386
+ executable in Smith only when Smith can validate the contract and enforce
387
+ deterministic stopping rules. This section does not restate every bounded
388
+ workflow helper; Orchestrator-Worker is a separate bounded delegation pattern.
389
+ Today that means:
390
+
391
+ | Loop kind | Executable? | Smith primitive | Boundary |
392
+ |---|---|---|---|
393
+ | Retry | Yes | `retry_on` | Local transition retry only. Durable scheduling and idempotency stay with the host. |
394
+ | Evaluator-Optimizer | Yes | `optimize` | Bounded refinement inside one transition using structured evaluator output. |
395
+ | Deterministic repair | Not native | Handwritten `compute` / `run` when exact semantics are owned by the workflow author. Deterministic route targets can be declared with `routes: [...]`. | No first-class repair contract or persisted repair counts yet. |
396
+ | Guarded state re-entry | Not native | Handwritten `compute` / `run` may declare and route to named transitions with `routes: [...]`. | No Smith-owned entry-count ledger, re-entry guard contract, or mutation policy yet. |
397
+ | Polling / wait | No | Host queue/timer plus Smith persistence helpers. | Smith must not model durable polling with sleeps, busy-waits, or `max_transitions` cycling. |
398
+
399
+ Do not hide a durable wait or unbounded repair policy inside `compute` / `run`.
400
+ Those primitives are synchronous deterministic steps inside the current workflow
401
+ runner. They are appropriate for local verification, normalization, failure
402
+ classification, explicit routing, and bounded retry-adjacent checks; they are
403
+ not a durable scheduler.
404
+
405
+ If a host or compiler wants deterministic repair, polling/wait, or guarded
406
+ state re-entry as reusable graph contracts, the contract must be added explicitly
407
+ before code generation claims executability. Deterministic `routes: [...]`
408
+ annotations make handwritten route targets inspectable, but they do not by
409
+ themselves provide bounded repair counts, mutation policy, or durable wait
410
+ semantics. At minimum that future contract needs bounded attempts, persisted
411
+ state keys where appropriate, deterministic exit behavior, graph-inspection
412
+ metadata, and practical execution coverage.
413
+
414
+ ## Example 9: Orchestrator-Worker
339
415
 
340
416
  Use `orchestrate` when you need an orchestrator that can emit structured tasks for workers and later decide when the system is done.
341
417
 
@@ -414,6 +490,9 @@ Why this is valuable:
414
490
  - tasks and outputs are structured
415
491
  - worker fan-out is controlled
416
492
  - exhaustion and malformed orchestrator output fail as first-class workflow failures
493
+ - graph inspection exposes orchestrator/worker bindings, bounds, schema labels,
494
+ decision/output contracts, serial worker-dispatch semantics, and
495
+ transition-level resume behavior
417
496
 
418
497
  Notes:
419
498
 
@@ -480,7 +559,7 @@ The yielded step object exposes a narrow, read-heavy surface:
480
559
 
481
560
  ### Behavior
482
561
 
483
- - **Routing**: `step.route_to` overrides `on_success`. If neither is set, normal state-based resolution applies. Named transitions that do not exist fail loudly with `WorkflowError`.
562
+ - **Routing**: `step.route_to` overrides `on_success`. If neither is set, normal state-based resolution applies. Named transitions that do not exist fail loudly with `WorkflowError`. When a deterministic transition declares `compute(routes: [...])` or `run(routes: [...])`, `step.route_to` must target one of those names; graph inspection validates the annotated targets before runtime.
484
563
  - **Failure**: `step.fail!` raises `Smith::DeterministicStepFailure` (extends `WorkflowError`) with `retryable`, `kind`, and `details` metadata. Routes through `on_failure` like any other step failure.
485
564
  - **Outcome**: `step.write_outcome(kind:, payload:)` stores a workflow-owned terminal payload without smuggling it through context. The payload is persisted with the workflow and surfaced on `RunResult.outcome`, `RunResult.outcome_kind`, and `RunResult.outcome_payload`.
486
565
  - **Context reads**: `step.context` returns an isolated snapshot of the workflow context at step start. Mutating that snapshot does not mutate workflow state. `step.read_context(key)` returns a merged view — pending `write_context` values override the snapshot. Use `read_context` when you need read-after-write coherence within the same step.
@@ -489,4 +568,4 @@ The yielded step object exposes a narrow, read-heavy surface:
489
568
  - **Persistence**: Context writes and written outcomes survive `to_state`/`from_state`. The block itself (a Proc) lives on the class-level Transition and is never serialized.
490
569
  - **Trace**: Emits `:deterministic_step` traces for start, success/routed, and failure. When a step writes an outcome, the trace includes `outcome_kind`.
491
570
  - **Mutual exclusivity**: `compute` and `run` cannot be combined with `execute`, `route`, `workflow`, `optimize`, or `orchestrate`. A transition declares exactly one primary execution body.
492
-
571
+ - **No hidden scheduler**: `compute` and `run` execute synchronously inside the current runner. Use them for bounded deterministic logic, not for durable polling or wake-up loops.
@@ -46,10 +46,10 @@ module Smith
46
46
 
47
47
  def build_model_chain(agent_class)
48
48
  primary = if agent_class.respond_to?(:model_block) && agent_class.model_block
49
- resolve_dynamic_model(agent_class)
50
- else
51
- agent_class.chat_kwargs[:model]
52
- end
49
+ resolve_dynamic_model(agent_class)
50
+ else
51
+ agent_class.chat_kwargs[:model]
52
+ end
53
53
  fallbacks = agent_class.fallback_models || []
54
54
  [primary, *fallbacks].compact
55
55
  end
@@ -103,8 +103,8 @@ module Smith
103
103
 
104
104
  declared = agent_class.inputs || []
105
105
  user_declared = declared - Smith::Agent::RESERVED_INPUT_NAMES
106
- user_declared.each_with_object({}) do |name, kwargs|
107
- kwargs[name] = @context[name]
106
+ user_declared.to_h do |name|
107
+ [name, @context[name]]
108
108
  end
109
109
  end
110
110
 
@@ -131,7 +131,9 @@ module Smith
131
131
 
132
132
  combined_contents = existing_system_contents + prepared_system_contents
133
133
  return if combined_contents.empty?
134
- return prepared_system_messages.each { |message| chat.add_message(message) } unless combined_contents.all?(String)
134
+ unless combined_contents.all?(String)
135
+ return prepared_system_messages.each { |message| chat.add_message(message) }
136
+ end
135
137
 
136
138
  if chat.respond_to?(:with_instructions)
137
139
  chat.with_instructions(combined_contents.join("\n\n"))
@@ -178,6 +180,8 @@ module Smith
178
180
  agent_result = Workflow::AgentResult.new(
179
181
  content: nil, input_tokens: input, output_tokens: output, cost: cost, model_used: model_id
180
182
  )
183
+ Thread.current[:smith_failed_agent_results] ||= []
184
+ Thread.current[:smith_failed_agent_results] << agent_result
181
185
  record_usage(agent_class, agent_result, :failed_attempt, model_id)
182
186
  end
183
187
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Agent
5
+ module Registry
6
+ module Introspection
7
+ def binding_for(name)
8
+ registry_monitor.synchronize do
9
+ key = normalize_key(name)
10
+ item = _container[key]
11
+ item ? Smith::Agent::RegistryBinding.new(key: key, item: item).to_h : nil
12
+ end
13
+ end
14
+
15
+ def bindings
16
+ registry_monitor.synchronize do
17
+ _container.each_with_object({}) do |(key, item), binding_map|
18
+ binding_map[key] = Smith::Agent::RegistryBinding.new(key: key, item: item).to_h
19
+ end.freeze
20
+ end
21
+ end
22
+ end
23
+
24
+ extend Introspection
25
+ end
26
+ end
27
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "dry-container"
4
4
  require "monitor"
5
+ require_relative "registry_binding"
5
6
 
6
7
  module Smith
7
8
  class Agent
@@ -14,14 +15,10 @@ module Smith
14
15
 
15
16
  def self.find(name)
16
17
  registry_monitor.synchronize do
17
- key = normalize_key(name)
18
- key?(key) ? resolve(key) : nil
18
+ registered_binding(normalize_key(name))
19
19
  end
20
20
  end
21
21
 
22
- # Override Dry::Container::Mixin#register to route agent classes
23
- # through ensure_registered while preserving full generic container
24
- # semantics (block, options) for non-agent registrations.
25
22
  def self.register(key, contents = nil, options = {}, &block)
26
23
  if block_given? || !(contents.is_a?(Class) && contents <= Smith::Agent)
27
24
  registry_monitor.synchronize { super(key, contents, options, &block) }
@@ -47,7 +44,7 @@ module Smith
47
44
  key = normalize_key(name)
48
45
 
49
46
  registry_monitor.synchronize do
50
- existing = key?(key) ? resolve(key) : nil
47
+ existing = registered_binding(key)
51
48
 
52
49
  if existing.nil?
53
50
  register_unchecked!(key, klass)
@@ -71,22 +68,15 @@ module Smith
71
68
  def self.fetch!(name, workflow_class: nil, transition_name: nil, role: :agent)
72
69
  registry_monitor.synchronize do
73
70
  key = normalize_key(name)
74
- return resolve(key) if key?(key)
75
-
76
- details = []
77
- details << "workflow #{workflow_class}" if workflow_class
78
- details << "transition :#{transition_name}" if transition_name
79
- suffix = details.empty? ? "" : " for #{details.join(', ')}"
71
+ binding = registered_binding(key)
72
+ return binding if binding
80
73
 
81
- raise Smith::WorkflowError, "unresolved #{role} :#{key}#{suffix}"
74
+ raise Smith::WorkflowError, "unresolved #{role} :#{key}#{fetch_suffix(workflow_class, transition_name)}"
82
75
  end
83
76
  end
84
77
 
85
- # Re-entrant lock (Monitor, not Mutex) so block-backed resolve
86
- # inside find/fetch! can safely re-enter the registry without
87
- # deadlocking on the same thread.
88
78
  def self.registry_monitor
89
- @_registry_monitor ||= Monitor.new
79
+ @registry_monitor ||= Monitor.new
90
80
  end
91
81
 
92
82
  def self.validate_agent_class!(klass)
@@ -115,6 +105,19 @@ module Smith
115
105
  end
116
106
  private_class_method :register_unchecked!
117
107
 
108
+ def self.registered_binding(key)
109
+ key?(key) ? resolve(key) : nil
110
+ end
111
+ private_class_method :registered_binding
112
+
113
+ def self.fetch_suffix(workflow_class, transition_name)
114
+ details = []
115
+ details << "workflow #{workflow_class}" if workflow_class
116
+ details << "transition :#{transition_name}" if transition_name
117
+ details.empty? ? "" : " for #{details.join(", ")}"
118
+ end
119
+ private_class_method :fetch_suffix
120
+
118
121
  def self.stale_reload_binding?(existing, klass)
119
122
  existing_name = existing.respond_to?(:name) ? existing.name : nil
120
123
  klass_name = klass.name
@@ -126,3 +129,5 @@ module Smith
126
129
  end
127
130
  end
128
131
  end
132
+
133
+ require_relative "registry/introspection"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ module Smith
6
+ class Agent
7
+ class RegistryBinding
8
+ extend Dry::Initializer
9
+
10
+ option :key
11
+ option :item
12
+
13
+ def to_h
14
+ {
15
+ key: key,
16
+ agent_class: agent_class,
17
+ call: call?,
18
+ raw_binding: raw_binding
19
+ }.freeze
20
+ end
21
+
22
+ private
23
+
24
+ def agent_class
25
+ return unless call? == false
26
+
27
+ raw_binding if raw_binding.is_a?(Class) && raw_binding <= Smith::Agent
28
+ end
29
+
30
+ def call?
31
+ options.fetch(:call, true)
32
+ end
33
+
34
+ def raw_binding
35
+ item.instance_variable_get(:@item)
36
+ end
37
+
38
+ def options
39
+ item.instance_variable_get(:@options) || {}
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/smith/agent.rb CHANGED
@@ -142,10 +142,9 @@ module Smith
142
142
 
143
143
  # Closes the `inputs` contract at the chat() boundary AND runs the
144
144
  # Smith::Models::Normalizer. Hook lives here (not in
145
- # Lifecycle#attempt_model) so direct callers like hadithi-xl's
146
- # InvokeCleaner.chat (which constructs a chat outside the workflow
147
- # lifecycle) are normalized too. Without this placement, Cleaner's
148
- # Opus 4.7 adaptive thinking translation would only fire for
145
+ # Lifecycle#attempt_model) so direct callers that construct a chat
146
+ # outside the workflow lifecycle are normalized too. Without this
147
+ # placement, model-specific request translation would only fire for
149
148
  # workflow-driven calls.
150
149
  #
151
150
  # Single profile lookup: resolved once via Models.find_or_infer and