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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +139 -0
  3. data/CODE_OF_CONDUCT.md +128 -0
  4. data/LICENSE +21 -0
  5. data/README.md +226 -0
  6. data/Rakefile +14 -0
  7. data/UPSTREAM_PROPOSAL.md +141 -0
  8. data/docs/CONFIGURATION.md +123 -0
  9. data/docs/PATTERNS.md +492 -0
  10. data/docs/PERSISTENCE.md +169 -0
  11. data/docs/TOOLS_AND_GUARDRAILS.md +140 -0
  12. data/docs/workflow_claim.md +58 -0
  13. data/exe/smith +7 -0
  14. data/lib/generators/smith/install/install_generator.rb +22 -0
  15. data/lib/generators/smith/install/templates/smith.rb.tt +44 -0
  16. data/lib/smith/agent/lifecycle.rb +264 -0
  17. data/lib/smith/agent/registry.rb +128 -0
  18. data/lib/smith/agent.rb +259 -0
  19. data/lib/smith/artifacts/file.rb +59 -0
  20. data/lib/smith/artifacts/memory.rb +75 -0
  21. data/lib/smith/artifacts/scoped_store.rb +29 -0
  22. data/lib/smith/artifacts.rb +5 -0
  23. data/lib/smith/budget/ledger.rb +42 -0
  24. data/lib/smith/budget.rb +5 -0
  25. data/lib/smith/cli.rb +82 -0
  26. data/lib/smith/context/observation_masking.rb +19 -0
  27. data/lib/smith/context/session.rb +42 -0
  28. data/lib/smith/context/state_injection.rb +24 -0
  29. data/lib/smith/context.rb +61 -0
  30. data/lib/smith/doctor/check.rb +12 -0
  31. data/lib/smith/doctor/checks/baseline.rb +84 -0
  32. data/lib/smith/doctor/checks/configuration.rb +56 -0
  33. data/lib/smith/doctor/checks/durability.rb +103 -0
  34. data/lib/smith/doctor/checks/live.rb +55 -0
  35. data/lib/smith/doctor/checks/models_registry.rb +66 -0
  36. data/lib/smith/doctor/checks/openai_api_mode.rb +51 -0
  37. data/lib/smith/doctor/checks/persistence.rb +99 -0
  38. data/lib/smith/doctor/checks/persistence_capabilities.rb +60 -0
  39. data/lib/smith/doctor/checks/persistence_registry.rb +82 -0
  40. data/lib/smith/doctor/checks/rails.rb +39 -0
  41. data/lib/smith/doctor/checks/serialization.rb +78 -0
  42. data/lib/smith/doctor/installer.rb +103 -0
  43. data/lib/smith/doctor/printer.rb +62 -0
  44. data/lib/smith/doctor/report.rb +39 -0
  45. data/lib/smith/doctor.rb +53 -0
  46. data/lib/smith/errors.rb +191 -0
  47. data/lib/smith/event.rb +11 -0
  48. data/lib/smith/events/.keep +0 -0
  49. data/lib/smith/events/bus.rb +60 -0
  50. data/lib/smith/events/step_completed.rb +11 -0
  51. data/lib/smith/events/subscription.rb +24 -0
  52. data/lib/smith/events.rb +5 -0
  53. data/lib/smith/guardrails/runner.rb +44 -0
  54. data/lib/smith/guardrails/url_verifier.rb +7 -0
  55. data/lib/smith/guardrails.rb +35 -0
  56. data/lib/smith/models/inference.rb +199 -0
  57. data/lib/smith/models/normalizer.rb +186 -0
  58. data/lib/smith/models/profile.rb +39 -0
  59. data/lib/smith/models.rb +132 -0
  60. data/lib/smith/persistence_adapters/active_record_store.rb +99 -0
  61. data/lib/smith/persistence_adapters/cache_store.rb +79 -0
  62. data/lib/smith/persistence_adapters/memory.rb +105 -0
  63. data/lib/smith/persistence_adapters/rails_cache.rb +20 -0
  64. data/lib/smith/persistence_adapters/redis_store.rb +136 -0
  65. data/lib/smith/persistence_adapters/retry.rb +42 -0
  66. data/lib/smith/persistence_adapters.rb +112 -0
  67. data/lib/smith/pricing.rb +65 -0
  68. data/lib/smith/providers/openai/responses.rb +315 -0
  69. data/lib/smith/providers/openai/routing.rb +67 -0
  70. data/lib/smith/providers/openai/tools_extensions.rb +106 -0
  71. data/lib/smith/railtie.rb +9 -0
  72. data/lib/smith/tasks/doctor.rake +38 -0
  73. data/lib/smith/tool/budget_enforcement.rb +33 -0
  74. data/lib/smith/tool/capability_builder.rb +18 -0
  75. data/lib/smith/tool/capture.rb +22 -0
  76. data/lib/smith/tool/compatibility.rb +72 -0
  77. data/lib/smith/tool/policy.rb +40 -0
  78. data/lib/smith/tool.rb +171 -0
  79. data/lib/smith/tools/think.rb +25 -0
  80. data/lib/smith/tools/url_fetcher.rb +16 -0
  81. data/lib/smith/tools/web_search.rb +17 -0
  82. data/lib/smith/tools.rb +5 -0
  83. data/lib/smith/trace/logger.rb +46 -0
  84. data/lib/smith/trace/memory.rb +53 -0
  85. data/lib/smith/trace/open_telemetry.rb +57 -0
  86. data/lib/smith/trace.rb +89 -0
  87. data/lib/smith/types.rb +16 -0
  88. data/lib/smith/version.rb +5 -0
  89. data/lib/smith/workflow/artifact_integration.rb +41 -0
  90. data/lib/smith/workflow/budget_integration.rb +105 -0
  91. data/lib/smith/workflow/claim.rb +118 -0
  92. data/lib/smith/workflow/data_volume_policy.rb +36 -0
  93. data/lib/smith/workflow/deadline_enforcement.rb +100 -0
  94. data/lib/smith/workflow/deterministic_execution.rb +53 -0
  95. data/lib/smith/workflow/deterministic_step.rb +57 -0
  96. data/lib/smith/workflow/dsl.rb +223 -0
  97. data/lib/smith/workflow/durability.rb +369 -0
  98. data/lib/smith/workflow/evaluator_optimizer.rb +220 -0
  99. data/lib/smith/workflow/event_integration.rb +24 -0
  100. data/lib/smith/workflow/execution.rb +127 -0
  101. data/lib/smith/workflow/execution_frame.rb +166 -0
  102. data/lib/smith/workflow/guardrail_integration.rb +40 -0
  103. data/lib/smith/workflow/nested_execution.rb +69 -0
  104. data/lib/smith/workflow/orchestrator_worker.rb +145 -0
  105. data/lib/smith/workflow/parallel.rb +50 -0
  106. data/lib/smith/workflow/parallel_execution.rb +75 -0
  107. data/lib/smith/workflow/persistence.rb +358 -0
  108. data/lib/smith/workflow/pipeline.rb +117 -0
  109. data/lib/smith/workflow/router.rb +53 -0
  110. data/lib/smith/workflow/transition.rb +208 -0
  111. data/lib/smith/workflow.rb +555 -0
  112. data/lib/smith.rb +254 -0
  113. data/script/profile_tool_results.rb +94 -0
  114. data/sig/smith.rbs +4 -0
  115. 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.
@@ -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"