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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +19 -0
- data/docs/AGENTS.md +3 -1
- data/docs/AGENT_LIFECYCLE.md +1 -1
- data/docs/AGENT_LOOP.md +2 -2
- data/docs/MESSAGES.md +1 -1
- data/lib/riffer/agent/run.rb +1 -1
- data/lib/riffer/agent/session.rb +7 -0
- data/lib/riffer/agent/structured_output/parser.rb +35 -0
- data/lib/riffer/agent/structured_output.rb +3 -5
- data/lib/riffer/messages/assistant/reasoning_part.rb +1 -1
- data/lib/riffer/providers/amazon_bedrock.rb +1 -1
- data/lib/riffer/providers/anthropic.rb +1 -1
- data/lib/riffer/providers/base.rb +1 -3
- data/lib/riffer/providers/open_ai.rb +2 -2
- data/lib/riffer/version.rb +1 -1
- data/rbs_collection.lock.yaml +2 -2
- data/sig/generated/riffer/agent/session.rbs +4 -0
- data/sig/generated/riffer/agent/structured_output/parser.rbs +17 -0
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +1 -1
- data/sig/manual/riffer/agent/structured_output/parser.rbs +5 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1379919ce4dcd42bd21286bb3447f32967cd94fff23cf5a3ed2e422b40a6cfc
|
|
4
|
+
data.tar.gz: 8d7860fa41acf5e89e82bd25f8f57477da356cb6948a4438d9ca0a7c71a6d959
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 882930df0b0f87cc9e2dbe3b792969db10b811a1c33d92ea5797d53564421177032c5ad75362247cbd5e012784fea7bf25c90d5de3a3ce54cc472af2c210e268
|
|
7
|
+
data.tar.gz: 9b23fe7c44c6d3fbbbe5ef165f2dc07c1a55ad2050dfd7b188cb674b9b3fe6ee45441583e8cc544092e3820c1c2a30f1f9809ce3ddd874d038180f94184990fd
|
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
|
-
|
|
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!"')
|
data/docs/AGENT_LIFECYCLE.md
CHANGED
|
@@ -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-
|
|
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
|
|
data/lib/riffer/agent/run.rb
CHANGED
data/lib/riffer/agent/session.rb
CHANGED
|
@@ -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 =
|
|
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-
|
|
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
|
-
|
|
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",
|
data/lib/riffer/version.rb
CHANGED
data/rbs_collection.lock.yaml
CHANGED
|
@@ -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.
|
|
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.
|
|
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-
|
|
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.
|
|
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
|