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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +91 -3
- data/README.md +69 -3
- data/UPSTREAM_PROPOSAL.md +19 -0
- data/docs/PATTERNS.md +85 -6
- data/lib/smith/agent/lifecycle.rb +11 -7
- data/lib/smith/agent/registry/introspection.rb +27 -0
- data/lib/smith/agent/registry.rb +22 -17
- data/lib/smith/agent/registry_binding.rb +43 -0
- data/lib/smith/agent.rb +3 -4
- data/lib/smith/doctor/checks/models_registry.rb +25 -14
- data/lib/smith/doctor/checks/persistence_capabilities.rb +37 -29
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/agent_result.rb +26 -0
- data/lib/smith/workflow/branch_env.rb +24 -0
- data/lib/smith/workflow/budget_integration.rb +27 -5
- data/lib/smith/workflow/deterministic_execution.rb +1 -1
- data/lib/smith/workflow/deterministic_step.rb +32 -4
- data/lib/smith/workflow/durability.rb +38 -12
- data/lib/smith/workflow/evaluator_optimizer.rb +18 -10
- data/lib/smith/workflow/event_integration.rb +3 -3
- data/lib/smith/workflow/execution.rb +8 -1
- data/lib/smith/workflow/fanout_execution.rb +121 -0
- data/lib/smith/workflow/graph/contract_helpers.rb +65 -0
- data/lib/smith/workflow/graph/fanout_contract.rb +140 -0
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +98 -0
- data/lib/smith/workflow/graph/optimization_contract.rb +96 -0
- data/lib/smith/workflow/graph/orchestration_contract.rb +83 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb +124 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +114 -0
- data/lib/smith/workflow/graph/runtime_readiness.rb +63 -0
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +87 -0
- data/lib/smith/workflow/graph/runtime_readiness_report.rb +65 -0
- data/lib/smith/workflow/graph/targets.rb +5 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +7 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +80 -18
- data/lib/smith/workflow/graph.rb +16 -1
- data/lib/smith/workflow/graph_dsl.rb +4 -0
- data/lib/smith/workflow/guardrail_integration.rb +45 -10
- data/lib/smith/workflow/nested_execution.rb +5 -4
- data/lib/smith/workflow/optimization_state.rb +13 -0
- data/lib/smith/workflow/orchestration_state.rb +13 -0
- data/lib/smith/workflow/orchestrator_worker.rb +5 -16
- data/lib/smith/workflow/parallel/cancellation.rb +9 -0
- data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
- data/lib/smith/workflow/parallel.rb +12 -16
- data/lib/smith/workflow/parallel_execution.rb +5 -1
- data/lib/smith/workflow/persistence.rb +37 -6
- data/lib/smith/workflow/retry_execution.rb +52 -0
- data/lib/smith/workflow/router.rb +15 -4
- data/lib/smith/workflow/run_result.rb +54 -0
- data/lib/smith/workflow/transition.rb +260 -24
- data/lib/smith/workflow/usage_entry.rb +30 -0
- data/lib/smith/workflow/worker_execution.rb +13 -0
- data/lib/smith/workflow.rb +40 -136
- data/lib/smith.rb +12 -0
- metadata +24 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b8a1ad3fc2cb40ef0287063f81e0bc01a6bca349b1625c4173daef763785202
|
|
4
|
+
data.tar.gz: b870358edea9686d15e164bb3d44e00edf8e50da8cb2d68b8a6a666bbc5d0f79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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`,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
107
|
-
|
|
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
|
-
|
|
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
|
data/lib/smith/agent/registry.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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}#{
|
|
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
|
-
@
|
|
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
|
|
146
|
-
#
|
|
147
|
-
#
|
|
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
|