smith-agents 0.4.2 → 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 +55 -3
- data/README.md +45 -2
- data/UPSTREAM_PROPOSAL.md +19 -0
- data/docs/PATTERNS.md +41 -1
- 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 +2 -0
- 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 +5 -8
- data/lib/smith/workflow/event_integration.rb +3 -3
- data/lib/smith/workflow/execution.rb +2 -0
- data/lib/smith/workflow/fanout_execution.rb +2 -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 +61 -20
- data/lib/smith/workflow/graph.rb +16 -1
- data/lib/smith/workflow/graph_dsl.rb +4 -0
- data/lib/smith/workflow/guardrail_integration.rb +20 -3
- 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 +3 -15
- data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
- data/lib/smith/workflow/parallel.rb +6 -15
- data/lib/smith/workflow/parallel_execution.rb +2 -0
- data/lib/smith/workflow/persistence.rb +37 -6
- data/lib/smith/workflow/router.rb +15 -4
- data/lib/smith/workflow/run_result.rb +54 -0
- data/lib/smith/workflow/transition.rb +90 -4
- 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 +9 -0
- metadata +21 -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,59 @@ 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.
|
|
10
62
|
|
|
11
63
|
## [0.4.2] - 2026-07-02
|
|
12
64
|
|
|
@@ -128,7 +180,7 @@ This release tracks two thematic refactors that together harden the agent-invoca
|
|
|
128
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.
|
|
129
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`.
|
|
130
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.
|
|
131
|
-
- `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.
|
|
132
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.
|
|
133
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.
|
|
134
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`.
|
|
@@ -188,4 +240,4 @@ This release tracks two thematic refactors that together harden the agent-invoca
|
|
|
188
240
|
|
|
189
241
|
## [0.1.0] - Initial public-track release
|
|
190
242
|
|
|
191
|
-
Initial pre-release
|
|
243
|
+
Initial pre-release. No formal changelog prior to the Phase A/B refactor.
|
data/README.md
CHANGED
|
@@ -17,7 +17,7 @@ environment before calling the slice complete.
|
|
|
17
17
|
|
|
18
18
|
```ruby
|
|
19
19
|
# Gemfile
|
|
20
|
-
gem "smith-agents", "~> 0.4.
|
|
20
|
+
gem "smith-agents", "~> 0.4.3", require: "smith"
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
```bash
|
|
@@ -103,6 +103,21 @@ end
|
|
|
103
103
|
|
|
104
104
|
The full pattern guide with working examples for each lives in [`docs/PATTERNS.md`](docs/PATTERNS.md).
|
|
105
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
|
+
|
|
106
121
|
## Workflow Graph Inspection
|
|
107
122
|
|
|
108
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.
|
|
@@ -118,6 +133,31 @@ report.metrics # => state, transition, reachability, and terminal-state co
|
|
|
118
133
|
|
|
119
134
|
Graph inspection is static and diagnostic-only. Runtime execution, persistence, progress projection, retries, and recovery remain host-owned concerns.
|
|
120
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
|
+
|
|
121
161
|
## Configuration
|
|
122
162
|
|
|
123
163
|
```ruby
|
|
@@ -162,7 +202,10 @@ result = ReplyWorkflow.run_persisted!(
|
|
|
162
202
|
)
|
|
163
203
|
```
|
|
164
204
|
|
|
165
|
-
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`):
|
|
166
209
|
|
|
167
210
|
- `:memory` — in-process Hash, intended for tests and `test_mode = true`
|
|
168
211
|
- `:redis` — Redis client; uses WATCH/MULTI/EXEC for CAS
|
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
|
@@ -276,6 +276,8 @@ What you get:
|
|
|
276
276
|
- one shared prepared input for the transition
|
|
277
277
|
- one shared transition result, so downstream joins remain explicit in the workflow
|
|
278
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
|
|
279
281
|
|
|
280
282
|
Use same-agent `parallel: true` for repeated homogeneous work. Use `fan_out` when branches are different agents with different responsibilities.
|
|
281
283
|
|
|
@@ -374,6 +376,40 @@ Why this matters:
|
|
|
374
376
|
- acceptance criteria are structured
|
|
375
377
|
- exhaustion, malformed evaluator output, and convergence without acceptance fail normally
|
|
376
378
|
- costs and token usage from the full loop roll into the workflow totals
|
|
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.
|
|
377
413
|
|
|
378
414
|
## Example 9: Orchestrator-Worker
|
|
379
415
|
|
|
@@ -454,6 +490,9 @@ Why this is valuable:
|
|
|
454
490
|
- tasks and outputs are structured
|
|
455
491
|
- worker fan-out is controlled
|
|
456
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
|
|
457
496
|
|
|
458
497
|
Notes:
|
|
459
498
|
|
|
@@ -520,7 +559,7 @@ The yielded step object exposes a narrow, read-heavy surface:
|
|
|
520
559
|
|
|
521
560
|
### Behavior
|
|
522
561
|
|
|
523
|
-
- **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.
|
|
524
563
|
- **Failure**: `step.fail!` raises `Smith::DeterministicStepFailure` (extends `WorkflowError`) with `retryable`, `kind`, and `details` metadata. Routes through `on_failure` like any other step failure.
|
|
525
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`.
|
|
526
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.
|
|
@@ -529,3 +568,4 @@ The yielded step object exposes a narrow, read-heavy surface:
|
|
|
529
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.
|
|
530
569
|
- **Trace**: Emits `:deterministic_step` traces for start, success/routed, and failure. When a step writes an outcome, the trace includes `outcome_kind`.
|
|
531
570
|
- **Mutual exclusivity**: `compute` and `run` cannot be combined with `execute`, `route`, `workflow`, `optimize`, or `orchestrate`. A transition declares exactly one primary execution body.
|
|
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.
|
|
@@ -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
|
|
@@ -36,29 +36,40 @@ module Smith
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Walk Smith::Agent::Registry. For each agent, extract
|
|
40
|
-
# id
|
|
41
|
-
#
|
|
42
|
-
#
|
|
39
|
+
# Walk Smith::Agent::Registry. For each agent, extract every static
|
|
40
|
+
# model id Smith can know at boot: the primary `model "..."` value and
|
|
41
|
+
# any static fallback models. Block-form primary models are skipped
|
|
42
|
+
# because they resolve per-attempt, but their static fallbacks still
|
|
43
|
+
# need coverage checks.
|
|
43
44
|
# Check whether find_or_infer returns a custom (non-default)
|
|
44
45
|
# Profile — meaning either an explicit override or an inference
|
|
45
46
|
# rule matched.
|
|
46
47
|
def uncovered_models
|
|
47
48
|
return [] unless defined?(Smith::Agent::Registry)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
next unless agent.is_a?(Class)
|
|
52
|
-
next unless agent.respond_to?(:chat_kwargs)
|
|
50
|
+
static_model_ids.uniq.reject { |model_id| covered_model?(model_id) }
|
|
51
|
+
end
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
def static_model_ids
|
|
54
|
+
Smith::Agent::Registry.each.with_object([]) do |(_key, agent), ids|
|
|
55
|
+
ids.concat(static_model_ids_for(agent)) if inspectable_agent?(agent)
|
|
56
56
|
end
|
|
57
|
+
end
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
def inspectable_agent?(agent)
|
|
60
|
+
agent.is_a?(Class) && agent.respond_to?(:chat_kwargs)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def static_model_ids_for(agent)
|
|
64
|
+
[
|
|
65
|
+
agent.chat_kwargs[:model],
|
|
66
|
+
*(agent.respond_to?(:fallback_models) ? agent.fallback_models : nil)
|
|
67
|
+
].compact
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def covered_model?(model_id)
|
|
71
|
+
Smith::Models.find(model_id) ||
|
|
72
|
+
(defined?(Smith::Models::Inference) && Smith::Models::Inference.profile_for(model_id))
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../persistence_adapters"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
module Doctor
|
|
5
7
|
module Checks
|
|
@@ -12,41 +14,16 @@ module Smith
|
|
|
12
14
|
module PersistenceCapabilities
|
|
13
15
|
module_function
|
|
14
16
|
|
|
15
|
-
OPTIONAL_CAPABILITIES =
|
|
17
|
+
OPTIONAL_CAPABILITIES = Smith::PersistenceAdapters::OPTIONAL_METHODS
|
|
16
18
|
|
|
17
19
|
def run(report)
|
|
18
20
|
adapter = resolve_adapter
|
|
19
|
-
if adapter.nil?
|
|
20
|
-
report.add(
|
|
21
|
-
name: "persistence.capabilities",
|
|
22
|
-
status: :warn,
|
|
23
|
-
message: "No persistence adapter configured",
|
|
24
|
-
detail: "Smith.config.persistence_adapter is nil and Smith.config.test_mode is false. " \
|
|
25
|
-
"Hosts using durable workflows must set persistence_adapter."
|
|
26
|
-
)
|
|
27
|
-
return
|
|
28
|
-
end
|
|
21
|
+
return report_missing_adapter(report) if adapter.nil?
|
|
29
22
|
|
|
30
23
|
missing = OPTIONAL_CAPABILITIES.reject { |cap| Smith::PersistenceAdapters.supports?(adapter, cap) }
|
|
24
|
+
return report_supported_capabilities(report, adapter) if missing.empty?
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
report.add(
|
|
34
|
-
name: "persistence.capabilities",
|
|
35
|
-
status: :pass,
|
|
36
|
-
message: "#{adapter.class.name} supports all optional persistence capabilities",
|
|
37
|
-
detail: "Supported: #{OPTIONAL_CAPABILITIES.join(', ')}"
|
|
38
|
-
)
|
|
39
|
-
else
|
|
40
|
-
report.add(
|
|
41
|
-
name: "persistence.capabilities",
|
|
42
|
-
status: :warn,
|
|
43
|
-
message: "#{adapter.class.name} missing optional capabilities: #{missing.join(', ')}",
|
|
44
|
-
detail: "Workflows using these capabilities fall back to non-versioned writes " \
|
|
45
|
-
"with a one-time warning per adapter class. Switch to RedisStore, " \
|
|
46
|
-
"ActiveRecordStore (with lock_version column), or the Memory adapter " \
|
|
47
|
-
"for full coverage."
|
|
48
|
-
)
|
|
49
|
-
end
|
|
26
|
+
report_missing_capabilities(report, adapter, missing)
|
|
50
27
|
end
|
|
51
28
|
|
|
52
29
|
def resolve_adapter
|
|
@@ -54,6 +31,37 @@ module Smith
|
|
|
54
31
|
rescue StandardError
|
|
55
32
|
nil
|
|
56
33
|
end
|
|
34
|
+
|
|
35
|
+
def report_missing_adapter(report)
|
|
36
|
+
report.add(
|
|
37
|
+
name: "persistence.capabilities",
|
|
38
|
+
status: :warn,
|
|
39
|
+
message: "No persistence adapter configured",
|
|
40
|
+
detail: "Smith.config.persistence_adapter is nil and Smith.config.test_mode is false. " \
|
|
41
|
+
"Hosts using durable workflows must set persistence_adapter."
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def report_supported_capabilities(report, adapter)
|
|
46
|
+
report.add(
|
|
47
|
+
name: "persistence.capabilities",
|
|
48
|
+
status: :pass,
|
|
49
|
+
message: "#{adapter.class.name} supports all optional persistence capabilities",
|
|
50
|
+
detail: "Supported: #{OPTIONAL_CAPABILITIES.join(", ")}"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def report_missing_capabilities(report, adapter, missing)
|
|
55
|
+
report.add(
|
|
56
|
+
name: "persistence.capabilities",
|
|
57
|
+
status: :warn,
|
|
58
|
+
message: "#{adapter.class.name} missing optional capabilities: #{missing.join(", ")}",
|
|
59
|
+
detail: "Smith will fall back where possible: non-versioned writes when store_versioned " \
|
|
60
|
+
"is missing, and payload updated_at parsing when heartbeat methods are missing. " \
|
|
61
|
+
"Use RedisStore or Memory for full versioning and heartbeat coverage; " \
|
|
62
|
+
"ActiveRecordStore currently covers optimistic locking when lock_version is present."
|
|
63
|
+
)
|
|
64
|
+
end
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
end
|
data/lib/smith/version.rb
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
# rubocop:disable Style/RedundantStructKeywordInit
|
|
6
|
+
AgentResult = Struct.new(
|
|
7
|
+
:content, :input_tokens, :output_tokens, :cost, :model_used,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
) do
|
|
10
|
+
def self.from_response(response, content, model_used: nil)
|
|
11
|
+
new(
|
|
12
|
+
content: content,
|
|
13
|
+
input_tokens: response.respond_to?(:input_tokens) ? response.input_tokens : nil,
|
|
14
|
+
output_tokens: response.respond_to?(:output_tokens) ? response.output_tokens : nil,
|
|
15
|
+
cost: nil,
|
|
16
|
+
model_used: model_used
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def usage_known?
|
|
21
|
+
!input_tokens.nil? && !output_tokens.nil?
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
# rubocop:enable Style/RedundantStructKeywordInit
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
# rubocop:disable Style/RedundantStructKeywordInit
|
|
6
|
+
BranchEnv = Struct.new(
|
|
7
|
+
:prepared_input, :guardrail_sources, :scoped_store, :branch_estimates, :deadline,
|
|
8
|
+
keyword_init: true
|
|
9
|
+
) do
|
|
10
|
+
def setup_thread
|
|
11
|
+
Smith::Tool.current_guardrails = guardrail_sources
|
|
12
|
+
Smith::Tool.current_deadline = deadline
|
|
13
|
+
Smith.scoped_artifacts = scoped_store
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def teardown_thread
|
|
17
|
+
Smith::Tool.current_guardrails = nil
|
|
18
|
+
Smith::Tool.current_deadline = nil
|
|
19
|
+
Smith.scoped_artifacts = nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
# rubocop:enable Style/RedundantStructKeywordInit
|
|
23
|
+
end
|
|
24
|
+
end
|