diff --git a/CHANGELOG.md b/CHANGELOG.md index 069c369..31a6a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.7.1 (2026-04-22) + +### Changed (behavioral, follow-up to v0.7.0) + +- **`Step::Base#run_once` no longer swallows adapter-phase `ArgumentError` as `:input_error`.** The previous blanket `rescue ArgumentError` was there to convert DSL misconfiguration (e.g. missing `prompt`) into an `:input_error` Result. Side effect: programmer bugs in adapter code that raised `ArgumentError` (wrong arity, bad config argument) were silently coerced into `:input_error` and retried as if the user had given bad input. Now the rescue is narrowed to the Runner-construction phase only — DSL configuration errors still produce `:input_error` (the `prompt has not been set` case is regression-tested), but `ArgumentError` raised from adapter code during `Runner#call` propagates to the caller. Input-type validation failures continue to produce `:input_error` through `InputValidator`'s own scoped rescue, unchanged. + ## 0.7.0 (2026-04-21) ### Breaking changes diff --git a/Gemfile.lock b/Gemfile.lock index 68c1daa..f87ef61 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - ruby_llm-contract (0.7.0) + ruby_llm-contract (0.7.1) dry-types (~> 1.7) ruby_llm (~> 1.0) ruby_llm-schema (~> 0.3) @@ -258,7 +258,7 @@ CHECKSUMS rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 ruby_llm (1.14.0) sha256=57c6f7034fc4a44504ea137d70f853b07824f1c1cdbe774ab3ab3522e7098deb - ruby_llm-contract (0.7.0) + ruby_llm-contract (0.7.1) ruby_llm-schema (0.3.0) sha256=a591edc5ca1b7f0304f0e2261de61ba4b3bea17be09f5cf7558153adfda3dec6 ruby_parser (3.22.0) sha256=1eb4937cd9eb220aa2d194e352a24dba90aef00751e24c8dfffdb14000f15d23 rubycritic (4.12.0) sha256=024fed90fe656fa939f6ea80aab17569699ac3863d0b52fd72cb99892247abc8 diff --git a/README.md b/README.md index 320ed37..8dcf422 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,9 @@ Full procedure with examples: **[Optimizing retry_policy](docs/guide/optimizing_ ## Roadmap -**v0.7 (current):** Sharpened retry semantics. `DEFAULT_RETRY_ON` now targets LLM output variance only (`:validation_failed`, `:parse_error`); transport errors are delegated to ruby_llm's Faraday retry. `AdapterCaller` narrowed to let programmer errors propagate instead of masking them as retries. Breaking change — see [CHANGELOG](CHANGELOG.md) for migration. +**v0.7.1 (current):** Follow-up — `Step::Base#run_once` no longer masks adapter-phase `ArgumentError` as `:input_error`. Programmer bugs in adapter code now propagate; DSL misconfiguration still becomes `:input_error` via narrower rescue. + +**v0.7.0:** Sharpened retry semantics. `DEFAULT_RETRY_ON` now targets LLM output variance only (`:validation_failed`, `:parse_error`); transport errors are delegated to ruby_llm's Faraday retry. `AdapterCaller` narrowed to let programmer errors propagate instead of masking them as retries. Breaking change — see [CHANGELOG](CHANGELOG.md) for migration. **v0.6:** "What should I do?" — `Step.recommend` returns optimal model, reasoning effort, and retry chain. Per-attempt `reasoning_effort` in retry policies. diff --git a/lib/ruby_llm/contract/step/base.rb b/lib/ruby_llm/contract/step/base.rb index 149d786..85b7a5b 100644 --- a/lib/ruby_llm/contract/step/base.rb +++ b/lib/ruby_llm/contract/step/base.rb @@ -186,20 +186,36 @@ def resolve_adapter(context) "{ |c| c.default_adapter = ... } or pass context: { adapter: ... }" end + # ADR-0021 deliverable 2: narrow ArgumentError rescue to DSL-setup phase only. + # + # DSL misconfiguration (e.g. `prompt has not been set`, missing required + # attributes) surfaces as ArgumentError when constructing Runner. We catch + # that and return :input_error — these are contract-definition issues the + # caller can handle as "bad input to the step definition". + # + # Runner#call itself does NOT get a blanket rescue: input-type validation + # failures return :input_error from within InputValidator; adapter/runtime + # programmer bugs (NoMethodError, adapter-code ArgumentError) must propagate + # instead of being silently masked as :input_error. def run_once(input, adapter:, model:, context_temperature: nil, extra_options: {}) effective_temp = context_temperature || temperature - Runner.new( - input_type: input_type, output_type: output_type, - prompt_block: prompt, contract_definition: effective_contract, - adapter: adapter, model: model, output_schema: output_schema, - max_output: max_output, max_input: max_input, max_cost: max_cost, - on_unknown_pricing: on_unknown_pricing, - temperature: effective_temp, extra_options: extra_options, - observers: class_observers - ).call(input) - rescue ArgumentError => e - Result.new(status: :input_error, raw_output: nil, parsed_output: nil, - validation_errors: [e.message]) + runner = + begin + Runner.new( + input_type: input_type, output_type: output_type, + prompt_block: prompt, contract_definition: effective_contract, + adapter: adapter, model: model, output_schema: output_schema, + max_output: max_output, max_input: max_input, max_cost: max_cost, + on_unknown_pricing: on_unknown_pricing, + temperature: effective_temp, extra_options: extra_options, + observers: class_observers + ) + rescue ArgumentError => e + return Result.new(status: :input_error, raw_output: nil, parsed_output: nil, + validation_errors: [e.message]) + end + + runner.call(input) end def log_result(result) diff --git a/lib/ruby_llm/contract/version.rb b/lib/ruby_llm/contract/version.rb index 044e181..fc8fbde 100644 --- a/lib/ruby_llm/contract/version.rb +++ b/lib/ruby_llm/contract/version.rb @@ -2,6 +2,6 @@ module RubyLLM module Contract - VERSION = "0.7.0" + VERSION = "0.7.1" end end diff --git a/spec/ruby_llm/contract/step/retry_integration_spec.rb b/spec/ruby_llm/contract/step/retry_integration_spec.rb index 1ba4582..df8c154 100644 --- a/spec/ruby_llm/contract/step/retry_integration_spec.rb +++ b/spec/ruby_llm/contract/step/retry_integration_spec.rb @@ -513,5 +513,31 @@ expect { step.run("hi", context: { adapter: adapter }) }.to raise_error(NoMethodError) end + + # ADR-0021 deliverable 2: programmer ArgumentError from adapter code must + # propagate, not be silently coerced into :input_error by Step::Base#run_once. + # Before the fix, a blanket `rescue ArgumentError` around the whole runner + # chain masked adapter bugs as "bad user input". + it "propagates ArgumentError from adapter code (programmer bug, not bad input)" do + adapter = Object.new + adapter.define_singleton_method(:call) do |**_opts| + raise ArgumentError, "adapter called with wrong arity — this is a bug" + end + + step = Class.new(RubyLLM::Contract::Step::Base) { prompt "{input}" } + + expect { step.run("hi", context: { adapter: adapter }) } + .to raise_error(ArgumentError, /adapter called with wrong arity/) + end + + it "still converts DSL misconfiguration ArgumentError to :input_error (prompt missing)" do + adapter = RubyLLM::Contract::Adapters::Test.new(response: "ok") + step = Class.new(RubyLLM::Contract::Step::Base) { output_type String } + + result = step.run("hi", context: { adapter: adapter }) + + expect(result.status).to eq(:input_error) + expect(result.validation_errors.first).to include("prompt has not been set") + end end end