riffer 0.50.0 → 0.51.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1700c8028d7de9b0e109fc259f904c8a8ff1e92964e23c90cd00574cd43a0bc2
4
- data.tar.gz: 1245b96f6a1490aa5cedd3d5e60ff062a8d70cc7330bd7ec01d0f027e05d65fa
3
+ metadata.gz: c1379919ce4dcd42bd21286bb3447f32967cd94fff23cf5a3ed2e422b40a6cfc
4
+ data.tar.gz: 8d7860fa41acf5e89e82bd25f8f57477da356cb6948a4438d9ca0a7c71a6d959
5
5
  SHA512:
6
- metadata.gz: dc40857b3085259180f014442cd8d2ec4c6f6f85d2c577cacbff6332176eb39e948de9e20b22732730e52ea4151016fcf23c3387de181f8b8ff27537df8cc21f
7
- data.tar.gz: '0152801a535973a01216aac4ff1a5b07554b2e8bde282a9dd3fc2c09408b0ddb7890953e7d60c21102d0889896190adf88d6822ef0ca0ff4a6564b688f7b4dd2'
6
+ metadata.gz: 882930df0b0f87cc9e2dbe3b792969db10b811a1c33d92ea5797d53564421177032c5ad75362247cbd5e012784fea7bf25c90d5de3a3ce54cc472af2c210e268
7
+ data.tar.gz: 9b23fe7c44c6d3fbbbe5ef165f2dc07c1a55ad2050dfd7b188cb674b9b3fe6ee45441583e8cc544092e3820c1c2a30f1f9809ce3ddd874d038180f94184990fd
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.50.0"
2
+ ".": "0.51.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.51.0](https://github.com/janeapp/riffer/compare/riffer/v0.50.1...riffer/v0.51.0) (2026-10-07)
9
+
10
+
11
+ ### Features
12
+
13
+ * **structured_output:** recover objects wrapped in code fences or prose ([#473](https://github.com/janeapp/riffer/issues/473)) ([59a6d40](https://github.com/janeapp/riffer/commit/59a6d40b08b9f18fcb4ebac24598925f94a9acea))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **agent:** apply the max_steps cap per run, not per conversation ([#478](https://github.com/janeapp/riffer/issues/478)) ([4948e6b](https://github.com/janeapp/riffer/commit/4948e6b9769df084bed8c8a29862067560f800bc))
19
+
20
+ ## [0.50.1](https://github.com/janeapp/riffer/compare/riffer/v0.50.0...riffer/v0.50.1) (2026-10-02)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * **providers:** replay reasoning parts with no text as empty strings ([#470](https://github.com/janeapp/riffer/issues/470)) ([a76d355](https://github.com/janeapp/riffer/commit/a76d3550d1e030b2d77118fb33e5d52271f00393))
26
+
8
27
  ## [0.50.0](https://github.com/janeapp/riffer/compare/riffer/v0.49.0...riffer/v0.50.0) (2026-09-28)
9
28
 
10
29
 
data/docs/AGENTS.md CHANGED
@@ -168,7 +168,9 @@ end
168
168
 
169
169
  The LLM response is automatically parsed and validated against the schema. Access the result via `response.structured_output`.
170
170
 
171
- When the response is not valid JSON or does not satisfy the schema, `response.structured_output` is `nil`, `response.outcome.reason` is `:invalid_structured_output`, and `response.outcome.detail` carries the parse or validation message:
171
+ If the response is not valid JSON on its own, Riffer recovers a JSON object wrapped in a code fence (```` ```json ````), surrounded by prose (`Here is the data: {...}`), or wrapped in formatting (`**{...}**`). It parses the span from the first `{` to the last `}`, so a reply whose surrounding text contains other braces, or that holds more than one object, is still rejected; `response.content` keeps the raw text.
172
+
173
+ When the response contains no well-formed JSON object or does not satisfy the schema, `response.structured_output` is `nil`, `response.outcome.reason` is `:invalid_structured_output`, and `response.outcome.detail` carries the parse or validation message:
172
174
 
173
175
  ```ruby
174
176
  response = SentimentAgent.generate('Analyze: "I love this!"')
@@ -351,7 +351,7 @@ end
351
351
 
352
352
  ### response.structured_output
353
353
 
354
- When structured output is configured, the LLM response is parsed as JSON and validated against the schema. The validated result is available as `response.structured_output`:
354
+ When structured output is configured, the LLM response is parsed as JSON and validated against the schema. A JSON object wrapped in a code fence, prose, or formatting is recovered (see [Agents — structured_output](AGENTS.md#structured_output)). The validated result is available as `response.structured_output`:
355
355
 
356
356
  ```ruby
357
357
  response = SentimentAgent.generate('Analyze: "I love this!"')
data/docs/AGENT_LOOP.md CHANGED
@@ -72,12 +72,12 @@ response = agent.generate('Approved, continue') # continues where it left off
72
72
 
73
73
  ### Max Steps Limit
74
74
 
75
- The `max_steps` class method caps the number of LLM call steps in the tool-use loop. When the step count reaches the limit, the loop interrupts automatically with reason `:max_steps`.
75
+ The `max_steps` class method caps the number of LLM call steps in the tool-use loop of a single run. Steps are counted from the last user message in the session, so each new user message starts a fresh budget — prior turns' assistant messages in the history do not count against it. When the step count reaches the limit, the loop interrupts automatically with reason `:max_steps`.
76
76
 
77
77
  - **When to use:** Safety net to prevent runaway tool-use loops — useful when agents have access to many tools or operate autonomously.
78
78
  - **Response:** `response.outcome.reason` is `:max_steps`.
79
79
  - **Streaming:** Yields an `Interrupt` event with `reason: :max_steps`.
80
- - **Resumable:** Yes. Call `generate('Continue')` or `stream('Continue')` on the same agent instance to resume. For cross-process resume, pass persisted messages as an array to a new agent. Pending tool calls are automatically executed before the LLM loop resumes.
80
+ - **Resumable:** Yes. Call `generate('Continue')` or `stream('Continue')` on the same agent instance to resume with a fresh budget, or call `generate` / `stream` with no prompt to resume against the remaining budget. For cross-process resume, pass persisted messages as an array to a new agent; the same rule applies. Pending tool calls are automatically executed before the LLM loop resumes.
81
81
 
82
82
  ```ruby
83
83
  class MyAgent < Riffer::Agent
data/docs/MESSAGES.md CHANGED
@@ -164,7 +164,7 @@ Each part carries:
164
164
  | `data` | `String?` | The opaque payload, for `:encrypted` parts |
165
165
  | `signature` | `String?` | The provider's signature over the part, when it issues one |
166
166
  | `id` | `String?` | The provider's identifier for the part, when it issues one |
167
- | `format` | `String?` | The wire format, owned by the adapter that produced the part (e.g. `"anthropic-claude-v1"`) |
167
+ | `format` | `String?` | The wire format, owned by the adapter that produced the part (e.g. `"anthropic-messages-v1"`) |
168
168
 
169
169
  A `type` outside the three values raises `Riffer::ArgumentError`. `format` is a free string riffer never validates — it exists so an adapter can tell its own parts apart from another adapter's.
170
170
 
@@ -68,7 +68,7 @@ module Riffer::Agent::Run
68
68
  end
69
69
 
70
70
  begin
71
- step = agent.session.steps
71
+ step = agent.session.steps_since_last_user_message
72
72
 
73
73
  reason = catch(:riffer_interrupt) do
74
74
  execute_pending_tool_calls(agent, tags)
@@ -143,6 +143,13 @@ class Riffer::Agent::Session
143
143
  @messages.count { |m| m.is_a?(Riffer::Messages::Assistant) }
144
144
  end
145
145
 
146
+ #--
147
+ #: () -> Integer
148
+ def steps_since_last_user_message
149
+ since_last_user = @messages.reverse_each.take_while { |m| !m.is_a?(Riffer::Messages::User) }
150
+ since_last_user.count { |m| m.is_a?(Riffer::Messages::Assistant) }
151
+ end
152
+
146
153
  #--
147
154
  #: () -> Riffer::Messages::Assistant?
148
155
  def final_assistant_message
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "json"
5
+
6
+ module Riffer::Agent::StructuredOutput::Parser
7
+ extend self
8
+
9
+ #--
10
+ #: (String) -> Hash[Symbol, untyped]?
11
+ def parse(content)
12
+ parse_object(content) || outermost_object(content)
13
+ end
14
+
15
+ private
16
+
17
+ #--
18
+ #: (String) -> Hash[Symbol, untyped]?
19
+ def outermost_object(content)
20
+ start = content.index("{")
21
+ stop = content.rindex("}")
22
+ return unless start && stop
23
+
24
+ parse_object(content[start..stop].to_s)
25
+ end
26
+
27
+ #--
28
+ #: (String) -> Hash[Symbol, untyped]?
29
+ def parse_object(json)
30
+ parsed = JSON.parse(json, symbolize_names: true)
31
+ parsed if parsed.is_a?(Hash)
32
+ rescue JSON::ParserError
33
+ nil
34
+ end
35
+ end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- require "json"
5
-
6
4
  class Riffer::Agent::StructuredOutput
7
5
  attr_reader :params #: Riffer::Params # @dynamic params
8
6
 
@@ -21,11 +19,11 @@ class Riffer::Agent::StructuredOutput
21
19
  #--
22
20
  #: (String) -> Riffer::Agent::StructuredOutput::Result
23
21
  def parse_and_validate(json_string)
24
- parsed = JSON.parse(json_string, symbolize_names: true)
22
+ parsed = Riffer::Agent::StructuredOutput::Parser.parse(json_string)
23
+ return Result.new(error: "JSON parse error: no JSON object found in the response") if parsed.nil?
24
+
25
25
  validated = @params.validate(parsed)
26
26
  Result.new(object: validated)
27
- rescue JSON::ParserError => e
28
- Result.new(error: "JSON parse error: #{e.message}")
29
27
  rescue Riffer::ValidationError => e
30
28
  Result.new(error: "Validation error: #{e.message}")
31
29
  end
@@ -12,7 +12,7 @@ class Riffer::Messages::Assistant::ReasoningPart
12
12
  attr_reader :signature #: String? # @dynamic signature
13
13
  attr_reader :id #: String? # @dynamic id
14
14
 
15
- # Owned by the producing adapter (e.g. <tt>"anthropic-claude-v1"</tt>); adapters replay only the
15
+ # Owned by the producing adapter (e.g. <tt>"anthropic-messages-v1"</tt>); adapters replay only the
16
16
  # formats they recognize, so it is never validated here.
17
17
  attr_reader :format #: String? # @dynamic format
18
18
 
@@ -480,7 +480,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
480
480
  data = part.data
481
481
  return { reasoning_content: { redacted_content: Base64.strict_decode64(data) } } if data
482
482
 
483
- { reasoning_content: { reasoning_text: { text: part.text, signature: part.signature }.compact } }
483
+ { reasoning_content: { reasoning_text: { text: part.text.to_s, signature: part.signature }.compact } }
484
484
  end
485
485
 
486
486
  #--
@@ -440,7 +440,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
440
440
  def convert_reasoning_part_to_anthropic_format(part)
441
441
  return { type: "redacted_thinking", data: part.data } if part.type == :encrypted
442
442
 
443
- { type: "thinking", thinking: part.text, signature: part.signature }
443
+ { type: "thinking", thinking: part.text.to_s, signature: part.signature }
444
444
  end
445
445
 
446
446
  #--
@@ -360,9 +360,7 @@ class Riffer::Providers::Base
360
360
  #--
361
361
  #: (String) -> Hash[Symbol, untyped]?
362
362
  def parse_structured_output(content)
363
- JSON.parse(content, symbolize_names: true)
364
- rescue JSON::ParserError
365
- nil
363
+ Riffer::Agent::StructuredOutput::Parser.parse(content)
366
364
  end
367
365
 
368
366
  #--
@@ -446,8 +446,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
446
446
  #--
447
447
  #: (Array[Riffer::Messages::Assistant::ReasoningPart]) -> Hash[Symbol, untyped]
448
448
  def convert_reasoning_item_to_openai_format(parts)
449
- summary = parts.filter_map { |part| { type: "summary_text", text: part.text } if part.type == :summary }
450
- content = parts.filter_map { |part| { type: "reasoning_text", text: part.text } if part.type == :text }
449
+ summary = parts.filter_map { |part| { type: "summary_text", text: part.text.to_s } if part.type == :summary }
450
+ content = parts.filter_map { |part| { type: "reasoning_text", text: part.text.to_s } if part.type == :text }
451
451
 
452
452
  {
453
453
  type: "reasoning",
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.50.0" #: String
5
+ VERSION = "0.51.0" #: String
6
6
  end
@@ -10,7 +10,7 @@ gems:
10
10
  remote: https://github.com/ruby/gem_rbs_collection.git
11
11
  repo_dir: gems
12
12
  - name: anthropic
13
- version: 1.71.0
13
+ version: 1.76.0
14
14
  source:
15
15
  type: rubygems
16
16
  - name: ast
@@ -170,7 +170,7 @@ gems:
170
170
  source:
171
171
  type: stdlib
172
172
  - name: openai
173
- version: 0.91.0
173
+ version: 0.97.0
174
174
  source:
175
175
  type: rubygems
176
176
  - name: openssl
@@ -57,6 +57,10 @@ class Riffer::Agent::Session
57
57
  # : () -> Integer
58
58
  def steps: () -> Integer
59
59
 
60
+ # --
61
+ # : () -> Integer
62
+ def steps_since_last_user_message: () -> Integer
63
+
60
64
  # --
61
65
  # : () -> Riffer::Messages::Assistant?
62
66
  def final_assistant_message: () -> Riffer::Messages::Assistant?
@@ -0,0 +1,17 @@
1
+ # Generated from lib/riffer/agent/structured_output/parser.rb with RBS::Inline
2
+
3
+ module Riffer::Agent::StructuredOutput::Parser
4
+ # --
5
+ # : (String) -> Hash[Symbol, untyped]?
6
+ def parse: (String) -> Hash[Symbol, untyped]?
7
+
8
+ private
9
+
10
+ # --
11
+ # : (String) -> Hash[Symbol, untyped]?
12
+ def outermost_object: (String) -> Hash[Symbol, untyped]?
13
+
14
+ # --
15
+ # : (String) -> Hash[Symbol, untyped]?
16
+ def parse_object: (String) -> Hash[Symbol, untyped]?
17
+ end
@@ -15,7 +15,7 @@ class Riffer::Messages::Assistant::ReasoningPart
15
15
 
16
16
  attr_reader id: String?
17
17
 
18
- # Owned by the producing adapter (e.g. <tt>"anthropic-claude-v1"</tt>); adapters replay only the
18
+ # Owned by the producing adapter (e.g. <tt>"anthropic-messages-v1"</tt>); adapters replay only the
19
19
  # formats they recognize, so it is never validated here.
20
20
  attr_reader format: String?
21
21
 
@@ -0,0 +1,5 @@
1
+ # `Riffer::Agent::StructuredOutput::Parser` uses `extend self`; rbs-inline doesn't emit that,
2
+ # so re-extend here to expose its instance methods as singleton methods.
3
+ module Riffer::Agent::StructuredOutput::Parser
4
+ extend ::Riffer::Agent::StructuredOutput::Parser
5
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riffer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.50.0
4
+ version: 0.51.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall
@@ -117,6 +117,7 @@ files:
117
117
  - lib/riffer/agent/session.rb
118
118
  - lib/riffer/agent/session/repair.rb
119
119
  - lib/riffer/agent/structured_output.rb
120
+ - lib/riffer/agent/structured_output/parser.rb
120
121
  - lib/riffer/agent/structured_output/result.rb
121
122
  - lib/riffer/config.rb
122
123
  - lib/riffer/config/amazon_bedrock.rb
@@ -265,6 +266,7 @@ files:
265
266
  - sig/generated/riffer/agent/session.rbs
266
267
  - sig/generated/riffer/agent/session/repair.rbs
267
268
  - sig/generated/riffer/agent/structured_output.rbs
269
+ - sig/generated/riffer/agent/structured_output/parser.rbs
268
270
  - sig/generated/riffer/agent/structured_output/result.rbs
269
271
  - sig/generated/riffer/config.rbs
270
272
  - sig/generated/riffer/config/amazon_bedrock.rbs
@@ -390,6 +392,7 @@ files:
390
392
  - sig/manual/riffer/agent/run.rbs
391
393
  - sig/manual/riffer/agent/serializer.rbs
392
394
  - sig/manual/riffer/agent/session/repair.rbs
395
+ - sig/manual/riffer/agent/structured_output/parser.rbs
393
396
  - sig/manual/riffer/evals/evaluator_runner.rbs
394
397
  - sig/manual/riffer/helpers/boolean.rbs
395
398
  - sig/manual/riffer/helpers/call_or_value.rbs