smith-agents 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +139 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/LICENSE +21 -0
- data/README.md +226 -0
- data/Rakefile +14 -0
- data/UPSTREAM_PROPOSAL.md +141 -0
- data/docs/CONFIGURATION.md +123 -0
- data/docs/PATTERNS.md +492 -0
- data/docs/PERSISTENCE.md +169 -0
- data/docs/TOOLS_AND_GUARDRAILS.md +140 -0
- data/docs/workflow_claim.md +58 -0
- data/exe/smith +7 -0
- data/lib/generators/smith/install/install_generator.rb +22 -0
- data/lib/generators/smith/install/templates/smith.rb.tt +44 -0
- data/lib/smith/agent/lifecycle.rb +264 -0
- data/lib/smith/agent/registry.rb +128 -0
- data/lib/smith/agent.rb +259 -0
- data/lib/smith/artifacts/file.rb +59 -0
- data/lib/smith/artifacts/memory.rb +75 -0
- data/lib/smith/artifacts/scoped_store.rb +29 -0
- data/lib/smith/artifacts.rb +5 -0
- data/lib/smith/budget/ledger.rb +42 -0
- data/lib/smith/budget.rb +5 -0
- data/lib/smith/cli.rb +82 -0
- data/lib/smith/context/observation_masking.rb +19 -0
- data/lib/smith/context/session.rb +42 -0
- data/lib/smith/context/state_injection.rb +24 -0
- data/lib/smith/context.rb +61 -0
- data/lib/smith/doctor/check.rb +12 -0
- data/lib/smith/doctor/checks/baseline.rb +84 -0
- data/lib/smith/doctor/checks/configuration.rb +56 -0
- data/lib/smith/doctor/checks/durability.rb +103 -0
- data/lib/smith/doctor/checks/live.rb +55 -0
- data/lib/smith/doctor/checks/models_registry.rb +66 -0
- data/lib/smith/doctor/checks/openai_api_mode.rb +51 -0
- data/lib/smith/doctor/checks/persistence.rb +99 -0
- data/lib/smith/doctor/checks/persistence_capabilities.rb +60 -0
- data/lib/smith/doctor/checks/persistence_registry.rb +82 -0
- data/lib/smith/doctor/checks/rails.rb +39 -0
- data/lib/smith/doctor/checks/serialization.rb +78 -0
- data/lib/smith/doctor/installer.rb +103 -0
- data/lib/smith/doctor/printer.rb +62 -0
- data/lib/smith/doctor/report.rb +39 -0
- data/lib/smith/doctor.rb +53 -0
- data/lib/smith/errors.rb +191 -0
- data/lib/smith/event.rb +11 -0
- data/lib/smith/events/.keep +0 -0
- data/lib/smith/events/bus.rb +60 -0
- data/lib/smith/events/step_completed.rb +11 -0
- data/lib/smith/events/subscription.rb +24 -0
- data/lib/smith/events.rb +5 -0
- data/lib/smith/guardrails/runner.rb +44 -0
- data/lib/smith/guardrails/url_verifier.rb +7 -0
- data/lib/smith/guardrails.rb +35 -0
- data/lib/smith/models/inference.rb +199 -0
- data/lib/smith/models/normalizer.rb +186 -0
- data/lib/smith/models/profile.rb +39 -0
- data/lib/smith/models.rb +132 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +99 -0
- data/lib/smith/persistence_adapters/cache_store.rb +79 -0
- data/lib/smith/persistence_adapters/memory.rb +105 -0
- data/lib/smith/persistence_adapters/rails_cache.rb +20 -0
- data/lib/smith/persistence_adapters/redis_store.rb +136 -0
- data/lib/smith/persistence_adapters/retry.rb +42 -0
- data/lib/smith/persistence_adapters.rb +112 -0
- data/lib/smith/pricing.rb +65 -0
- data/lib/smith/providers/openai/responses.rb +315 -0
- data/lib/smith/providers/openai/routing.rb +67 -0
- data/lib/smith/providers/openai/tools_extensions.rb +106 -0
- data/lib/smith/railtie.rb +9 -0
- data/lib/smith/tasks/doctor.rake +38 -0
- data/lib/smith/tool/budget_enforcement.rb +33 -0
- data/lib/smith/tool/capability_builder.rb +18 -0
- data/lib/smith/tool/capture.rb +22 -0
- data/lib/smith/tool/compatibility.rb +72 -0
- data/lib/smith/tool/policy.rb +40 -0
- data/lib/smith/tool.rb +171 -0
- data/lib/smith/tools/think.rb +25 -0
- data/lib/smith/tools/url_fetcher.rb +16 -0
- data/lib/smith/tools/web_search.rb +17 -0
- data/lib/smith/tools.rb +5 -0
- data/lib/smith/trace/logger.rb +46 -0
- data/lib/smith/trace/memory.rb +53 -0
- data/lib/smith/trace/open_telemetry.rb +57 -0
- data/lib/smith/trace.rb +89 -0
- data/lib/smith/types.rb +16 -0
- data/lib/smith/version.rb +5 -0
- data/lib/smith/workflow/artifact_integration.rb +41 -0
- data/lib/smith/workflow/budget_integration.rb +105 -0
- data/lib/smith/workflow/claim.rb +118 -0
- data/lib/smith/workflow/data_volume_policy.rb +36 -0
- data/lib/smith/workflow/deadline_enforcement.rb +100 -0
- data/lib/smith/workflow/deterministic_execution.rb +53 -0
- data/lib/smith/workflow/deterministic_step.rb +57 -0
- data/lib/smith/workflow/dsl.rb +223 -0
- data/lib/smith/workflow/durability.rb +369 -0
- data/lib/smith/workflow/evaluator_optimizer.rb +220 -0
- data/lib/smith/workflow/event_integration.rb +24 -0
- data/lib/smith/workflow/execution.rb +127 -0
- data/lib/smith/workflow/execution_frame.rb +166 -0
- data/lib/smith/workflow/guardrail_integration.rb +40 -0
- data/lib/smith/workflow/nested_execution.rb +69 -0
- data/lib/smith/workflow/orchestrator_worker.rb +145 -0
- data/lib/smith/workflow/parallel.rb +50 -0
- data/lib/smith/workflow/parallel_execution.rb +75 -0
- data/lib/smith/workflow/persistence.rb +358 -0
- data/lib/smith/workflow/pipeline.rb +117 -0
- data/lib/smith/workflow/router.rb +53 -0
- data/lib/smith/workflow/transition.rb +208 -0
- data/lib/smith/workflow.rb +555 -0
- data/lib/smith.rb +254 -0
- data/script/profile_tool_results.rb +94 -0
- data/sig/smith.rbs +4 -0
- metadata +258 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c6e0cbabc0bd3db4447206b0326a96996b5b3a6b46ec82e0533298f4074b0659
|
|
4
|
+
data.tar.gz: 1e4f9346f46675db971627676f5d85621120d3387e95fab5258ef5de88f22939
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c91ea9dfb9d21c284278a46a12380b94aba48091370a9db358ea2a5ddb96a21b1a18a453d6d0b431f2839a153a20e5d93bd9a611573f93c99efb59b9e48a21dd
|
|
7
|
+
data.tar.gz: f75d0d058c722e640c1299cb62beaafb7a3051f13d786460b289187a7c4a15c5e32aa2f2f5d8b68369d5b5d1b8d6e479147bbf2beb668a2273e740ef7a21f882
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Smith are documented in this file.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Smith is pre-1.0 and under active development; expect occasional contract tightening between minor versions until 1.0.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
No unreleased changes.
|
|
10
|
+
|
|
11
|
+
## [0.4.0] - 2026-06-24
|
|
12
|
+
|
|
13
|
+
Two more host-ergonomic primitives that close the deferred-from-0.3.0 backlog: `Workflow.stuck_for?` for liveness probing and `Context.persist :auto` for write-tracked context persistence. Both are purely additive.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `Smith::Workflow.stuck_for?(persistence_key:, threshold:, since: nil, adapter:)` — answers whether a workflow attempt is genuinely stuck. Path A (payload present): returns `true` when the workflow is NOT terminal and the heartbeat age (or fallback `payload['updated_at']` age) exceeds `threshold`. Path B (no payload + caller-supplied `since:`): returns `true` when `since` is older than `threshold`, handling the pre-persist gap window where consumers mark a status to `:processing` before Smith records any state. Terminal detection uses the real state-graph rule (`class.transitions_from(state).empty? && next_transition_name.nil?`).
|
|
18
|
+
- `Smith::Workflow.heartbeat_age(persistence_key:, adapter:)` — bare age accessor returning seconds since last heartbeat, or `nil` when no payload/heartbeat exists. Intended for dashboards.
|
|
19
|
+
- `Smith::PersistenceAdapter#record_heartbeat(key, ttl:)` and `#last_heartbeat(key)` — new optional adapter methods. Both join `OPTIONAL_METHODS`; `REQUIRED_METHODS` stays `%i[store fetch delete]`. v1 ships heartbeat write+read on `Memory` and `RedisStore`. `Workflow#persist!` calls `record_heartbeat` after a successful `store`/`store_versioned`; adapters that don't implement the methods fall through to `payload['updated_at']` parsing with a one-time warning per adapter class.
|
|
20
|
+
- `Smith::Context.persist :auto` — declarative mode where the workflow's persisted context is computed from the keys actually written via `DeterministicStep#write_context`. Backward-compat preserved: `persist :a, :b` continues to mean explicit allow-list. `persist :auto, also: [:user_message]` declares the input seed list (initial-context keys must be enumerated here to round-trip). The workflow records each `write_context` key into `@persisted_keys` (a `Set`, protected by a Mutex for parallel safety) and slices through it on `:auto`-mode `persisted_context`.
|
|
21
|
+
- `Smith::Workflow#persisted_keys` — frozen read-only accessor for the recorded auto-tracked keys.
|
|
22
|
+
- New top-level `to_state` field `:persisted_keys` (sorted Array of Symbols). Round-trips through restore. Pre-`:auto` payloads with no key list seed `@persisted_keys` from the keys present in the stored context Hash, treating that as lossless migration.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `Smith::Context.persist` signature gains an `also:` keyword. Passing `also:` without `:auto` raises `Smith::WorkflowError`. Passing `:auto` with additional positional args also raises.
|
|
27
|
+
- `Smith::Workflow#persist!` now calls `record_heartbeat` on adapters that support it. Failed `store_versioned` (PersistenceVersionConflict) does NOT bump the heartbeat.
|
|
28
|
+
- `Smith::PersistenceAdapters::RedisStore#delete` now deletes both the payload key and the heartbeat sidecar key in a single `DEL` call.
|
|
29
|
+
- `Smith::Workflow.to_state` includes the new `:persisted_keys` field unconditionally (forward-compatible payload shape for explicit-mode workflows too).
|
|
30
|
+
|
|
31
|
+
### Test coverage
|
|
32
|
+
|
|
33
|
+
- Default suite: 857 examples, 0 failures (+22 stuck_for/heartbeat, +19 persist :auto).
|
|
34
|
+
- `SMITH_AR_SPECS=1` suite: 872 examples, 0 failures.
|
|
35
|
+
|
|
36
|
+
## [0.3.0] - 2026-06-24
|
|
37
|
+
|
|
38
|
+
Two host-ergonomic primitives that absorb boilerplate consumer Execution wrappers were reinventing. Both are purely additive — existing workflows continue to work without changes.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `Smith::Workflow::ExecutionFrame` — absorbs the five-flag bookkeeping pattern (`claimed`, `result_obtained`, `recorded`, `intentional_retry`, `finalize_succeeded`) duplicated across host Execution wrappers. The host yields its per-attempt work into `ExecutionFrame.run`, records lifecycle milestones via `mark_*!` setters, and the frame's ensure invokes `on_clear` (when the canonical clear decision says so) and `always_ensure` (whenever claimed, independent of the clear decision; covers the advisory-lock-release case). `workflow:` accepts a Smith::Workflow instance OR a callable that resolves lazily at ensure-time. `OrderingError` and `AlreadyRun` inherit from `Smith::Error`, not `Smith::WorkflowError`, so host `rescue Smith::WorkflowError` blocks cannot silently downgrade ordering bugs. Logger fallback chain: explicit `logger:` kwarg, then `Smith.config.logger`, then a last-resort `Logger.new($stderr)`.
|
|
43
|
+
- `Smith::Workflow::Claim.atomic` — AASM-aware claim helper. Wraps `record.public_send(transition_via)` inside `transaction_owner.transaction` (default: `model_class`). Inside the transaction: `lock.find(id)`, case-on-status, invoke the AASM event when status is in `from_statuses`. Returns the reloaded record on success, `nil` when status is in `terminal_statuses`, raises `Smith::Workflow::Claim::UnexpectedStatus` when status is outside `from_statuses ∪ terminal_statuses` (default `:raise`; opt into `:ignore` or `:log` via `on_unexpected_status:`). Raises `ArgumentError` when `transition_via:` is nil AND the model responds to `.aasm`, preventing silent AASM-callback drops.
|
|
44
|
+
- `Smith::Workflow::Claim.cas` — single-statement CAS via `update_all` with `where(status: from_statuses)`. Returns the reloaded record or `nil` if rowcount is zero. Stamps `updated_at` via the injected `now:` lambda. Does NOT invoke AASM events; intended for non-AASM CAS sites.
|
|
45
|
+
- Both `Claim` strategies load lazily — `lib/smith/workflow/claim.rb` does NOT const-reference `::ActiveRecord` at module load. Both raise `Smith::Workflow::Claim::AdapterUnavailable` when invoked without AR present, so Smith stays gem-load-time decoupled from AR.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- `activerecord ~> 8.0` and `sqlite3 ~> 2.0` added as development/test dependencies (NOT runtime). The Claim spec harness in `spec/support/active_record_harness.rb` is ENV-gated behind `SMITH_AR_SPECS=1`; when unset, `:ar`-tagged examples are excluded so the default suite never loads AR.
|
|
50
|
+
|
|
51
|
+
### Test coverage
|
|
52
|
+
|
|
53
|
+
- Default suite: 816 examples, 0 failures (existing + ExecutionFrame + Claim load-hygiene).
|
|
54
|
+
- `SMITH_AR_SPECS=1` suite: 831 examples, 0 failures (adds 15 `:ar`-tagged Claim specs).
|
|
55
|
+
|
|
56
|
+
## [0.2.0] - 2026-06-24
|
|
57
|
+
|
|
58
|
+
This release tracks two thematic refactors that together harden the agent-invocation and persistence layers, plus a third slice that closes EvaluatorOptimizer ergonomics gaps surfaced by host adoption:
|
|
59
|
+
|
|
60
|
+
- **Phase A**: replaces the Opus 4.7 monkey-patch with a generic, library-shipped capability-aware normalizer; fixes the previously-broken cross-provider fallback path (Claude → gpt-5.5 + tools + thinking) by routing through `/v1/responses` when supported or gracefully dropping incompatible tools otherwise.
|
|
61
|
+
- **Phase B**: hardens the persistence layer with TTL, retry, optimistic locking, schema versioning, seed-drift validation, step-in-progress idempotency, and an in-process Memory adapter for test isolation.
|
|
62
|
+
- **Phase C**: extends EvaluatorOptimizer with `evaluator_context: :inject_state`, a `before_eval:` deterministic hook, and `on_exhaustion:` / `on_converged:` / `on_threshold:` graceful-exit modes; adds `Smith::Errors.retryable?` to own the retryable-error classification host-side.
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
#### Phase C: EvaluatorOptimizer ergonomics + retry classification
|
|
67
|
+
|
|
68
|
+
- `Smith::Errors.retryable?(error)` classifier owned at the library level. `AgentError` and `DeadlineExceeded` are always-retryable; `DeterministicStepFailure` and `ToolGuardrailFailed` honor their `retryable` attribute (opt-in at the raise site); all other Smith errors and non-Smith errors return false. Replaces ad-hoc case statements in host Execution / Job wrappers.
|
|
69
|
+
- `Smith::Errors.retryable_classes` returns the frozen always-retryable class list for ActiveJob `retry_on` allow-lists.
|
|
70
|
+
- `optimize evaluator_context: :inject_state` opts the evaluator into the same `prepared_input` the generator was built with. The evaluator now sees the workflow's `seed_messages` plus `Context.inject_state` observations (voice profiles, research artifacts, source URLs) plus the candidate as a turn-local user message. Default `nil` preserves the legacy candidate-only payload.
|
|
71
|
+
- `optimize before_eval: proc { |state, context| ... }` runs after the generator produces a candidate and before the evaluator is invoked. The hook receives the `OptimizationState` and the live workflow `@context` (mutable). Typical use: a deterministic validator (regex kill-list, structural check) writes findings into context so the evaluator surfaces them deterministically instead of rediscovering by feel each round.
|
|
72
|
+
- `optimize on_exhaustion:`, `on_converged:`, `on_threshold:` graceful-exit modes. Each accepts `:raise` (default, legacy behavior), `:return_last` (return the most recent candidate as the step output), or a callable receiving the `OptimizationState`. Lets refinement workflows opt into "best of N rounds" semantics instead of terminal `WorkflowError`.
|
|
73
|
+
|
|
74
|
+
#### Phase A: capability-aware request shaping
|
|
75
|
+
|
|
76
|
+
- `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.
|
|
77
|
+
- `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`.
|
|
78
|
+
- `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.
|
|
79
|
+
- `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.
|
|
80
|
+
- `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.
|
|
81
|
+
- `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.
|
|
82
|
+
- `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`.
|
|
83
|
+
- `Smith::Providers::OpenAI::Routing` prepend installed onto `RubyLLM::Providers::OpenAI`; routes to `Smith::Providers::OpenAI::Responses` when `@params[:openai_api_mode] == :responses`.
|
|
84
|
+
- `Smith::Providers::OpenAI::Responses` adapter for `/v1/responses` payload assembly + HTTP dispatch + response parsing. Vendored from [crmne/ruby_llm PR #770](https://github.com/crmne/ruby_llm/pull/770) at pinned SHA `a84517db65d3774c6b129dc88032fe32c8dbc722` (render/parse helpers verbatim with namespace requalification; `complete`, `format_role`, and `resolve_effort` are Smith-authored glue). Retirement path documented in `UPSTREAM_PROPOSAL.md`. Streaming is intentionally not yet supported; block-given calls raise `NotImplementedError` with a clear workaround.
|
|
85
|
+
- `Smith::Providers::OpenAI::ToolsExtensions` adapter for OpenAI tool format helpers consumed by Responses (response_tool_for, parse_response_tool_calls, build_response_tool_choice). Vendored from the same PR + SHA.
|
|
86
|
+
- `Smith.config.openai_api_mode` setting (`:auto` | `:off`, default `:auto`) with constructor validation.
|
|
87
|
+
- `Smith.config.trace_normalizer` setting (default true) gating `:normalizer_decision` trace events.
|
|
88
|
+
- Doctor checks: `models.coverage` (warns when registered agents reference models without an explicit profile or matching Inference rule) and `config.openai_api_mode` (warns when `:auto` is configured but the Responses adapter is absent).
|
|
89
|
+
- `UPSTREAM_PROPOSAL.md` documenting the proposed RubyLLM extensions (`Capabilities::Profile`, `Provider.before_complete` hook, public `without_thinking` / `without_temperature` chat builders) and the retirement checklist for Smith files that go away when the upstream API ships.
|
|
90
|
+
|
|
91
|
+
#### Phase B: persistence hardening
|
|
92
|
+
|
|
93
|
+
- `Smith::PersistenceAdapters::Memory` in-process Hash adapter (Monitor-synchronized), supports TTL via stamped expiry and optimistic locking via `store_versioned`. Auto-selected by `Smith.persistence_adapter` when `persistence_adapter` is nil and `Smith.config.test_mode = true`, so test suites can skip wiring Redis/Rails.cache in `spec_helper.rb`.
|
|
94
|
+
- `Smith::PersistenceAdapters::Retry.with_retries(operation:, transient:)` exponential-backoff wrapper used by all I/O-bound adapters. Each adapter declares its own `TRANSIENT_ERRORS` constant matching its backend (Redis transient errors via class-name lookup, CacheStore Errno errors, ActiveRecord connection errors); Memory adapter passes an empty list (in-process, no transient errors).
|
|
95
|
+
- `Smith::PersistenceIOError` raised after retry exhaustion, wrapping the underlying cause with `#operation` and `#cause` fields.
|
|
96
|
+
- `Smith.config.persistence_ttl` global TTL setting (Integer/Float seconds; nil = no expiry).
|
|
97
|
+
- `Smith.config.persistence_retry_policy` setting (defaults: `{ attempts: 3, base_delay: 0.1, max_delay: 1.0 }`).
|
|
98
|
+
- `Smith.config.test_mode` setting (default false).
|
|
99
|
+
- `Smith::PersistenceAdapters::OPTIONAL_METHODS = %i[store_versioned]` and `Smith::PersistenceAdapters.supports?(adapter, capability)` for capability introspection. `warn_missing_versioning(adapter)` issues a one-time per-adapter-class warning when an adapter doesn't implement `store_versioned`.
|
|
100
|
+
- `store_versioned(key, payload, expected_version:, ttl:)` on RedisStore (WATCH/MULTI/EXEC), Memory (Monitor-synchronized version compare), and ActiveRecordStore (Rails optimistic locking on a configurable `lock_version` column). CacheStore deliberately does not implement it (cache backends lack uniform CAS semantics).
|
|
101
|
+
- `Smith::PersistenceVersionConflict` raised on stale `expected_version` or detected concurrent writes; carries `#key #expected #actual` fields. Workflow's `@persistence_version` stays at the pre-failure value so callers can rescue → restore → retry.
|
|
102
|
+
- `@persistence_version` ivar in `Smith::Workflow` (default 0); incremented after each successful persist; restored from the persisted payload. Pre-versioning payloads (missing key) are treated as version 0 for backward compatibility.
|
|
103
|
+
- `Workflow#persist!` consults `Smith::PersistenceAdapters.supports?(adapter, :store_versioned)` and falls back to plain `store` with the one-time warning when absent.
|
|
104
|
+
- `Workflow.persistence_schema_version(N)` DSL (default 1) + `Workflow.migrate_from(N) { |payload| ... }` blocks. `to_state` carries `:schema_version`; restore walks `migrate_if_needed` one step at a time. Defensive cursor advancement (Smith advances `:schema_version` if a migration block forgets to). Downgrades and unbridged gaps raise `Smith::PersistenceSchemaMismatch` with `#workflow #stored #current` fields.
|
|
105
|
+
- `Workflow.seed_validation(:strict | :warn | :off)` DSL (default `:off`) gating SHA256 digest comparison of `seed_messages` at restore time. `@seed_digest` is computed at construction and persisted in `to_state`; restore re-evaluates the seed builder against the restored `@context` and compares. `:strict` raises `Smith::SeedMismatch`; `:warn` logs via `Smith.config.logger&.warn`. Default `:off` reflects that many seed builders are non-deterministic (timestamps, UUIDs, request-scoped data) and would surface false drift on every restore.
|
|
106
|
+
- `Workflow.idempotency_mode(:strict | :lax)` DSL (default `:lax`). Strict mode stamps a `@step_in_progress` marker before each pre-advance persist and clears it after the post-advance persist. Restore raises `Smith::StepInProgressOnRestore` (with `#persistence_key`) when the marker is set under `:strict`, signalling that a prior worker crashed mid-step and re-running could double-execute non-idempotent agent calls or tools.
|
|
107
|
+
- `Workflow.persistence_ttl(seconds)` DSL (positive Numeric) for per-workflow TTL override. Resolution precedence: class DSL > `Smith.config.persistence_ttl` > nil. `Workflow#persist!` forwards `ttl:` to the adapter only when non-nil, so external duck-typed adapters with bare `store(key, payload)` keep working as long as the host doesn't opt into TTL.
|
|
108
|
+
- TTL pass-through in all native-supporting adapters: RedisStore (`ex:`), CacheStore (`expires_in:`, RailsCache inherits), Memory (stamped expiry). ActiveRecordStore TTL is deferred (would need an `expires_at` column + sweeper job; documented inline).
|
|
109
|
+
- Doctor check: `persistence.capabilities` warns when the configured adapter is missing optional capabilities (currently `store_versioned`), surfacing the silent fallback eagerly under the `:rails_persistence` profile.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
|
|
113
|
+
- RubyLLM dependency bumped from `~> 1.14` to `~> 1.15`. RubyLLM 1.15 ships `claude-opus-4-7` and `gpt-5.5` aliases natively, so Smith no longer needs to runtime-register them.
|
|
114
|
+
- `Smith::Workflow::UsageEntry`, `AgentResult`, and `BranchEnv` Structs converted to `keyword_init: true` for forward/backward compatibility on persisted state. `UsageEntry.from_h` slices the input hash to known members (unknown keys silently dropped, missing keys default to nil) and symbolizes `:agent_name` + `:attempt_kind` for backward compatibility with callers that consume them as Symbols.
|
|
115
|
+
- `Smith::Agent.chat()` is now a Smith-owned override that resolves the model's `Smith::Models::Profile`, injects reserved input values into `runtime_context`, nil-fills declared user inputs, calls `super`, then runs `Smith::Models::Normalizer.apply!`. Direct callers no longer require special handling.
|
|
116
|
+
- `Smith::Agent.inputs` getter returns the union of user-declared and reserved input names (frozen); setter merges (RubyLLM's bare `@input_names = names` would replace and lose reserved names).
|
|
117
|
+
- Persistence adapters now wrap `store/fetch/delete/store_versioned` in `Smith::PersistenceAdapters::Retry.with_retries`. The Memory adapter is intentionally skipped (in-process, no transient errors).
|
|
118
|
+
- `Smith.persistence_adapter` caching now keys on a signature that includes `test_mode` so toggling it invalidates the cached adapter.
|
|
119
|
+
|
|
120
|
+
### Removed
|
|
121
|
+
|
|
122
|
+
- `Smith::RubyLLMModels` module (`lib/smith/ruby_llm_models.rb`) and its spec. Replaced by `Smith::Models` + `Smith::Models::Inference`.
|
|
123
|
+
- `Smith::RubyLLMAnthropicOpus47Compat` monkey-patch on `RubyLLM::Providers::Anthropic`. Replaced by `Smith::Models::Normalizer.apply!` at chat construction.
|
|
124
|
+
|
|
125
|
+
### Migration notes
|
|
126
|
+
|
|
127
|
+
- Hosts that constructed `Smith::Workflow::UsageEntry.new(usage_id, agent_name, …)` with positional arguments must switch to keyword form (`UsageEntry.new(usage_id:, agent_name:, …)`). Same for `AgentResult` and `BranchEnv`. The `from_h` constructor is unchanged and continues to accept legacy persisted hashes.
|
|
128
|
+
- Hosts that opt into `Smith.config.openai_api_mode = :auto` (now the default) and hit `(gpt-5 family + tools + thinking)` will route via `/v1/responses` using the vendored adapter. Streaming over the Responses endpoint is not yet supported; block-given completions raise `NotImplementedError` with a clear workaround (either drop the block for sync mode, or set `openai_api_mode = :off` for graceful tool-dropping via chat-completions). Sync (non-streaming) completions work end-to-end against OpenAI's `/v1/responses`.
|
|
129
|
+
- Hosts running ActiveRecordStore with optimistic locking enabled must add a `lock_version` integer column (default 0) to their AR-backed persistence model:
|
|
130
|
+
```ruby
|
|
131
|
+
add_column :workflow_states, :lock_version, :integer, default: 0
|
|
132
|
+
```
|
|
133
|
+
Smith raises `ArgumentError` with the exact migration command if the column is absent and `store_versioned` is invoked.
|
|
134
|
+
- Hosts using cache-backed persistence adapters (`CacheStore`, `RailsCache`, `SolidCache`) get a one-time per-adapter-class warning at first persist that optimistic locking is unavailable; the workflow falls back to plain `store` without raising. Switch to `RedisStore`, `ActiveRecordStore` (with `lock_version`), or `Memory` (tests) for full optimistic-locking coverage.
|
|
135
|
+
- Hosts subclassing `Smith::Tool` and declaring tools that are designed for specific provider families should add `compatible_with` declarations so the normalizer can route or drop appropriately. Tools without a declaration are treated as universally compatible (preserves pre-refactor behavior for hosts that haven't opted in).
|
|
136
|
+
|
|
137
|
+
## [0.1.0] - Initial public-track release
|
|
138
|
+
|
|
139
|
+
Initial pre-release tagged for the Hadithi consumer's local-path dependency. No formal changelog prior to the Phase A/B refactor.
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity
|
|
10
|
+
and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
|
18
|
+
community include:
|
|
19
|
+
|
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
+
and learning from the experience
|
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the
|
|
26
|
+
overall community
|
|
27
|
+
|
|
28
|
+
Examples of unacceptable behavior include:
|
|
29
|
+
|
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or
|
|
31
|
+
advances of any kind
|
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
+
* Public or private harassment
|
|
34
|
+
* Publishing others' private information, such as a physical or email
|
|
35
|
+
address, without their explicit permission
|
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
+
professional setting
|
|
38
|
+
|
|
39
|
+
## Enforcement Responsibilities
|
|
40
|
+
|
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
+
or harmful.
|
|
45
|
+
|
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
+
decisions when appropriate.
|
|
50
|
+
|
|
51
|
+
## Scope
|
|
52
|
+
|
|
53
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
+
an individual is officially representing the community in public spaces.
|
|
55
|
+
Examples of representing our community include using an official e-mail address,
|
|
56
|
+
posting via an official social media account, or acting as an appointed
|
|
57
|
+
representative at an online or offline event.
|
|
58
|
+
|
|
59
|
+
## Enforcement
|
|
60
|
+
|
|
61
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
+
reported to the community leaders responsible for enforcement at
|
|
63
|
+
samuelralak@hey.com.
|
|
64
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
|
65
|
+
|
|
66
|
+
All community leaders are obligated to respect the privacy and security of the
|
|
67
|
+
reporter of any incident.
|
|
68
|
+
|
|
69
|
+
## Enforcement Guidelines
|
|
70
|
+
|
|
71
|
+
Community leaders will follow these Community Impact Guidelines in determining
|
|
72
|
+
the consequences for any action they deem in violation of this Code of Conduct:
|
|
73
|
+
|
|
74
|
+
### 1. Correction
|
|
75
|
+
|
|
76
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
77
|
+
unprofessional or unwelcome in the community.
|
|
78
|
+
|
|
79
|
+
**Consequence**: A private, written warning from community leaders, providing
|
|
80
|
+
clarity around the nature of the violation and an explanation of why the
|
|
81
|
+
behavior was inappropriate. A public apology may be requested.
|
|
82
|
+
|
|
83
|
+
### 2. Warning
|
|
84
|
+
|
|
85
|
+
**Community Impact**: A violation through a single incident or series
|
|
86
|
+
of actions.
|
|
87
|
+
|
|
88
|
+
**Consequence**: A warning with consequences for continued behavior. No
|
|
89
|
+
interaction with the people involved, including unsolicited interaction with
|
|
90
|
+
those enforcing the Code of Conduct, for a specified period of time. This
|
|
91
|
+
includes avoiding interactions in community spaces as well as external channels
|
|
92
|
+
like social media. Violating these terms may lead to a temporary or
|
|
93
|
+
permanent ban.
|
|
94
|
+
|
|
95
|
+
### 3. Temporary Ban
|
|
96
|
+
|
|
97
|
+
**Community Impact**: A serious violation of community standards, including
|
|
98
|
+
sustained inappropriate behavior.
|
|
99
|
+
|
|
100
|
+
**Consequence**: A temporary ban from any sort of interaction or public
|
|
101
|
+
communication with the community for a specified period of time. No public or
|
|
102
|
+
private interaction with the people involved, including unsolicited interaction
|
|
103
|
+
with those enforcing the Code of Conduct, is allowed during this period.
|
|
104
|
+
Violating these terms may lead to a permanent ban.
|
|
105
|
+
|
|
106
|
+
### 4. Permanent Ban
|
|
107
|
+
|
|
108
|
+
**Community Impact**: Demonstrating a pattern of violation of community
|
|
109
|
+
standards, including sustained inappropriate behavior, harassment of an
|
|
110
|
+
individual, or aggression toward or disparagement of classes of individuals.
|
|
111
|
+
|
|
112
|
+
**Consequence**: A permanent ban from any sort of public interaction within
|
|
113
|
+
the community.
|
|
114
|
+
|
|
115
|
+
## Attribution
|
|
116
|
+
|
|
117
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
118
|
+
version 2.0, available at
|
|
119
|
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
120
|
+
|
|
121
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
122
|
+
enforcement ladder](https://github.com/mozilla/diversity).
|
|
123
|
+
|
|
124
|
+
[homepage]: https://www.contributor-covenant.org
|
|
125
|
+
|
|
126
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
|
127
|
+
https://www.contributor-covenant.org/faq. Translations are available at
|
|
128
|
+
https://www.contributor-covenant.org/translations.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Samuel Ralak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Smith
|
|
2
|
+
|
|
3
|
+
Workflow-first multi-agent orchestration for Ruby. Smith sits on top of `RubyLLM` and adds explicit state machines, typed contracts, budgets, guardrails, persistence, tools, and tracing for production agent systems.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> Smith is pre-1.0. Expect contract tightening between minor versions. Pin to an exact version in production.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Gemfile
|
|
12
|
+
gem "smith-agents", "~> 0.2.0", require: "smith"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The Ruby module namespace stays `Smith::`; only the gem name is namespaced because `smith` on RubyGems is taken. The `require: "smith"` in the Gemfile tells bundler to load the actual file name.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "ruby_llm"
|
|
25
|
+
require "smith"
|
|
26
|
+
|
|
27
|
+
RubyLLM.configure do |config|
|
|
28
|
+
config.openai_api_key = ENV.fetch("OPENAI_API_KEY")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ReplyAgent < Smith::Agent
|
|
32
|
+
register_as :reply_agent
|
|
33
|
+
model "gpt-4.1-nano"
|
|
34
|
+
|
|
35
|
+
instructions { "Write a concise, professional reply." }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class ReplyContext < Smith::Context
|
|
39
|
+
persist :user_message
|
|
40
|
+
inject_state { |p| "User message: #{p[:user_message]}" }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class ReplyWorkflow < Smith::Workflow
|
|
44
|
+
context_manager ReplyContext
|
|
45
|
+
initial_state :idle
|
|
46
|
+
state :done
|
|
47
|
+
state :failed
|
|
48
|
+
|
|
49
|
+
transition :reply, from: :idle, to: :done do
|
|
50
|
+
execute :reply_agent
|
|
51
|
+
on_failure :fail
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result = ReplyWorkflow.new(context: { user_message: "Charged twice." }).run!
|
|
56
|
+
result.state # => :done
|
|
57
|
+
result.output # => assistant reply
|
|
58
|
+
result.steps # => [{ transition: :reply, from: :idle, to: :done, output: ... }]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Core Concepts
|
|
62
|
+
|
|
63
|
+
| Concept | Purpose |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `Smith::Agent` | A `RubyLLM` agent plus model, instructions, output schema, tools, budget, and fallback models. Identifies itself to the workflow via `register_as :name`. |
|
|
66
|
+
| `Smith::Workflow` | A state machine of named transitions. Each transition calls an agent, runs deterministic code, routes, or composes a nested workflow. |
|
|
67
|
+
| `Smith::Context` | Declares which workflow context keys persist across restore, and how those keys become agent-visible input via `inject_state`. |
|
|
68
|
+
| `Smith::Tool` | A `RubyLLM` tool plus provider-compatibility metadata and guardrail hooks. |
|
|
69
|
+
| Persistence adapters | Host-owned storage. Smith ships `Memory`, `RedisStore`, `CacheStore`, `RailsCache`, `ActiveRecordStore`. |
|
|
70
|
+
| Trace adapters | Host-owned observability. Smith ships `Memory`, `Logger`, `OpenTelemetry`. |
|
|
71
|
+
|
|
72
|
+
Agents register at class load. In Rails, register workflow-facing agents in a `to_prepare` hook so autoload doesn't drop them:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# config/initializers/smith_agents.rb
|
|
76
|
+
Rails.application.config.to_prepare do
|
|
77
|
+
ReplyAgent
|
|
78
|
+
TriageAgent
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Patterns
|
|
83
|
+
|
|
84
|
+
| Pattern | DSL | Use case |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| Single execute | `execute :agent` | One agent call per transition. |
|
|
87
|
+
| Pipeline | sequential transitions | Multi-step workflow with explicit success/failure routing. |
|
|
88
|
+
| Router | `route :classifier, routes: {...}` | Branch on a classifier agent's output. |
|
|
89
|
+
| Parallel fan-out | `execute :agent, parallel: true` | Concurrent agent calls under one ledger. |
|
|
90
|
+
| Nested workflow | `workflow OtherWorkflow` | Reuse a subflow as one transition. |
|
|
91
|
+
| Evaluator-Optimizer | `optimize generator:, evaluator:, ...` | Generate-then-critique refinement loops. |
|
|
92
|
+
| Orchestrator-Worker | `orchestrate orchestrator:, worker:, ...` | Dynamic task fan-out with delegation rounds. |
|
|
93
|
+
| Deterministic | `compute { |step| ... }` | Pure Ruby step inside the state machine. |
|
|
94
|
+
|
|
95
|
+
The full pattern guide with working examples for each lives in [`docs/PATTERNS.md`](docs/PATTERNS.md).
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
require "logger"
|
|
101
|
+
require "smith"
|
|
102
|
+
|
|
103
|
+
Smith.configure do |config|
|
|
104
|
+
config.logger = Logger.new($stdout)
|
|
105
|
+
config.trace_adapter = Smith::Trace::Memory.new
|
|
106
|
+
config.artifact_store = Smith::Artifacts::Memory.new
|
|
107
|
+
|
|
108
|
+
# Persistence
|
|
109
|
+
config.persistence_adapter = :rails_cache
|
|
110
|
+
config.persistence_options = { namespace: "smith" }
|
|
111
|
+
config.persistence_ttl = 1.day.to_i
|
|
112
|
+
config.persistence_retry_policy = { attempts: 3, base_delay: 0.1, max_delay: 1.0 }
|
|
113
|
+
|
|
114
|
+
# OpenAI /v1/responses routing for gpt-5 + tools + thinking. :auto (default) or :off.
|
|
115
|
+
config.openai_api_mode = :auto
|
|
116
|
+
|
|
117
|
+
config.pricing = {
|
|
118
|
+
"gpt-4.1-nano" => { input_cost_per_token: 1.0e-7, output_cost_per_token: 4.0e-7 }
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
All settings are optional for a first run. See [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) for the full reference.
|
|
124
|
+
|
|
125
|
+
## Persistence and Resume
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# Persist after every advance
|
|
129
|
+
result = ReplyWorkflow.run_persisted!(
|
|
130
|
+
context: { user_message: "..." },
|
|
131
|
+
adapter: Smith.persistence_adapter
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Resume later
|
|
135
|
+
result = ReplyWorkflow.run_persisted!(
|
|
136
|
+
key: "ticket:T-1042",
|
|
137
|
+
adapter: Smith.persistence_adapter
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Built-in adapters (all support TTL where the backend allows; `Redis`, `ActiveRecord`, `Memory` also support optimistic locking via `store_versioned`):
|
|
142
|
+
|
|
143
|
+
- `:memory` — in-process Hash, intended for tests and `test_mode = true`
|
|
144
|
+
- `:redis` — Redis client; uses WATCH/MULTI/EXEC for CAS
|
|
145
|
+
- `:rails_cache`, `:solid_cache` — Rails cache backends
|
|
146
|
+
- `:cache_store` — any object responding to `write/read/delete`
|
|
147
|
+
- `:active_record` — keyed ActiveRecord model with `lock_version` column for CAS
|
|
148
|
+
|
|
149
|
+
See [`docs/PERSISTENCE.md`](docs/PERSISTENCE.md) for schema versioning, seed-drift validation, and the `idempotency_mode :strict` step-in-progress contract.
|
|
150
|
+
|
|
151
|
+
## Tools and Guardrails
|
|
152
|
+
|
|
153
|
+
Smith ships `Tools::WebSearch`, `Tools::UrlFetcher`, and `Tools::Think`. Tools declare provider compatibility via `compatible_with`; Smith's normalizer routes or drops them per-attempt.
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class SearchAgent < Smith::Agent
|
|
157
|
+
register_as :search_agent
|
|
158
|
+
model "claude-opus-4-7"
|
|
159
|
+
tools Smith::Tools::WebSearch, Smith::Tools::UrlFetcher
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Guardrails run as input/output gates around agent calls. See [`docs/TOOLS_AND_GUARDRAILS.md`](docs/TOOLS_AND_GUARDRAILS.md).
|
|
164
|
+
|
|
165
|
+
## Budgets and Deadlines
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
class BudgetedWorkflow < Smith::Workflow
|
|
169
|
+
budget total_tokens: 10_000, total_cost: 0.50, wall_clock_ms: 30_000
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Budgets reserve serially at each step and reconcile after the agent call. Parallel branches reserve scoped envelopes that release back to the parent ledger. The `Workflow::RunResult` carries `total_tokens`, `total_cost`, and per-call `usage_entries`.
|
|
174
|
+
|
|
175
|
+
## Doctor
|
|
176
|
+
|
|
177
|
+
After adding Smith, verify the integration:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Plain Ruby
|
|
181
|
+
smith doctor # offline checks
|
|
182
|
+
smith doctor --live # live provider call
|
|
183
|
+
smith doctor --durability # persistence round-trip
|
|
184
|
+
smith install # scaffold config/smith.rb
|
|
185
|
+
|
|
186
|
+
# Rails
|
|
187
|
+
bin/rails smith:doctor
|
|
188
|
+
bin/rails smith:doctor:live
|
|
189
|
+
bin/rails smith:doctor:durability
|
|
190
|
+
bin/rails generate smith:install
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Doctor verifies: Smith loads, RubyLLM loads, minimal workflow boots, configuration is non-empty, serialization round-trips, persistence adapter works, and (with `--live`) a real provider call succeeds.
|
|
194
|
+
|
|
195
|
+
## Capability-aware request shaping
|
|
196
|
+
|
|
197
|
+
Smith ships a per-attempt normalizer that translates the request payload to whatever the resolved model's provider family expects:
|
|
198
|
+
|
|
199
|
+
- Anthropic Opus 4.7+ adaptive thinking via `output_config[:effort]`
|
|
200
|
+
- Anthropic 4.0–4.6 budget_tokens
|
|
201
|
+
- OpenAI gpt-5 family reasoning_effort with `/v1/responses` routing when tools + thinking are combined
|
|
202
|
+
- Gemini 2.5+ budget_tokens
|
|
203
|
+
|
|
204
|
+
Override the inferred profile per-app via `Smith::Models.register(Profile.new(...))`. Hosts pin to specific model_ids by registering profiles; Smith never hardcodes model_ids in the library.
|
|
205
|
+
|
|
206
|
+
## Errors and retry
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
Smith::Errors.retryable?(error)
|
|
210
|
+
# AgentError, DeadlineExceeded => true (always)
|
|
211
|
+
# DeterministicStepFailure, ToolGuardrailFailed => honors error.retryable
|
|
212
|
+
# everything else => false
|
|
213
|
+
|
|
214
|
+
Smith::Errors.retryable_classes
|
|
215
|
+
# => [Smith::AgentError, Smith::DeadlineExceeded] (for ActiveJob retry_on)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
bundle install
|
|
222
|
+
bundle exec rspec
|
|
223
|
+
bundle exec rubocop
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
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.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
task default: %i[spec rubocop]
|
|
13
|
+
|
|
14
|
+
load "lib/smith/tasks/doctor.rake"
|