riffer 0.50.1 → 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: f6bfdc8b4676c2d368fd17ff5b709e802549a01580845e9c12c9f352d395e128
4
- data.tar.gz: 1567b5498abacc5fe2c820dee04d18bee016465108eea6c1172f03954359bdc6
3
+ metadata.gz: c1379919ce4dcd42bd21286bb3447f32967cd94fff23cf5a3ed2e422b40a6cfc
4
+ data.tar.gz: 8d7860fa41acf5e89e82bd25f8f57477da356cb6948a4438d9ca0a7c71a6d959
5
5
  SHA512:
6
- metadata.gz: 78c30c4798848f219b2c10f756d2c0caca5a95f2ab705660789a5e557284aa275e9d203a18e37427be63d8cee6858d90a88cb3da7aaf59abff2271a532ed65ec
7
- data.tar.gz: a8e8b4a891b42b3b1f83ba50a4fe6affe3f9ecf78a158de8d6696c89ed71c88a2549b5a6f72c134743914abb69e0a34d88e953b700d166be6626d8b92b6a139d
6
+ metadata.gz: 882930df0b0f87cc9e2dbe3b792969db10b811a1c33d92ea5797d53564421177032c5ad75362247cbd5e012784fea7bf25c90d5de3a3ce54cc472af2c210e268
7
+ data.tar.gz: 9b23fe7c44c6d3fbbbe5ef165f2dc07c1a55ad2050dfd7b188cb674b9b3fe6ee45441583e8cc544092e3820c1c2a30f1f9809ce3ddd874d038180f94184990fd
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.50.1"
2
+ ".": "0.51.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ 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
+
8
20
  ## [0.50.1](https://github.com/janeapp/riffer/compare/riffer/v0.50.0...riffer/v0.50.1) (2026-10-02)
9
21
 
10
22
 
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
@@ -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
@@ -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
  #--
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.50.1" #: 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
@@ -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.1
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