riffer 0.45.0 → 0.46.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 +17 -0
- data/docs/AGENTS.md +13 -0
- data/docs/AGENT_LIFECYCLE.md +39 -14
- data/docs/AGENT_LOOP.md +8 -9
- data/docs/CONFIGURATION.md +24 -17
- data/docs/GUARDRAILS.md +4 -4
- data/docs/MESSAGES.md +4 -3
- data/docs/STREAM_EVENTS.md +1 -1
- data/docs/TOOL_ADVANCED.md +2 -0
- data/docs/TRACING.md +3 -1
- data/docs/providers/MOCK_PROVIDER.md +18 -11
- data/lib/riffer/agent/outcome.rb +52 -0
- data/lib/riffer/agent/response.rb +12 -33
- data/lib/riffer/agent/run.rb +54 -16
- data/lib/riffer/agent/session.rb +2 -0
- data/lib/riffer/messages/assistant.rb +24 -2
- data/lib/riffer/messages/base.rb +11 -19
- data/lib/riffer/providers/anthropic.rb +5 -0
- data/lib/riffer/providers/base.rb +1 -0
- data/lib/riffer/runner/fibers.rb +12 -8
- data/lib/riffer/version.rb +1 -1
- data/sig/_private/async.rbs +4 -0
- data/sig/generated/riffer/agent/outcome.rbs +41 -0
- data/sig/generated/riffer/agent/response.rbs +11 -26
- data/sig/generated/riffer/agent/run.rbs +18 -7
- data/sig/generated/riffer/messages/assistant.rbs +14 -2
- data/sig/generated/riffer/runner/fibers.rbs +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac304f75fface8720f1e2565ea2064b643abb2f1c29f5a9dfe5281820e9d3986
|
|
4
|
+
data.tar.gz: de5695cffa30697e0356de921227caffdab12ff806e143166369c36e3ae010d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfe9da5a5cb0a8a1147846766acc79b518a9f7025d1d95fbfddce3604e2315afafc04762b339879eee2db875aba5cc296056045f213f47a17cb9f730e17f3e07
|
|
7
|
+
data.tar.gz: b548a4b44b001b79ee295969af5a868f50d0e847a183dc9433e5b23d4b1e7c9effed3ea64a76a8f5022db959a65394d8f8ff28bba9a6d46484841322483ebe02
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.46.0](https://github.com/janeapp/riffer/compare/riffer/v0.45.0...riffer/v0.46.0) (2026-09-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* Response#blocked?, Response#interrupted?, and Response#interrupt_reason are removed. Check response.outcome.reason for :guardrail_blocked, :interrupted, or :max_steps instead; response.tripwire still carries the full tripwire object. Response.new now requires outcome:.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* report how a run ended through Response#outcome ([#425](https://github.com/janeapp/riffer/issues/425)) ([2ba2157](https://github.com/janeapp/riffer/commit/2ba2157668cd4a2b013da190dd647ae131accfa3))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **anthropic:** merge caller output_config with structured output ([#422](https://github.com/janeapp/riffer/issues/422)) ([e1a60e6](https://github.com/janeapp/riffer/commit/e1a60e6f082dbf7d4ee9d151e96e56bdf3f26359))
|
|
23
|
+
* **runner:** make Fibers runner safe inside a running Async reactor ([#424](https://github.com/janeapp/riffer/issues/424)) ([47d3706](https://github.com/janeapp/riffer/commit/47d3706dbeac1e456bbf8365361cf16741b09adf))
|
|
24
|
+
|
|
8
25
|
## [0.45.0](https://github.com/janeapp/riffer/compare/riffer/v0.44.0...riffer/v0.45.0) (2026-09-03)
|
|
9
26
|
|
|
10
27
|
|
data/docs/AGENTS.md
CHANGED
|
@@ -168,6 +168,19 @@ 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:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
response = SentimentAgent.generate('Analyze: "I love this!"')
|
|
175
|
+
|
|
176
|
+
if response.outcome.success?
|
|
177
|
+
response.structured_output # => {sentiment: "positive", score: 0.95}
|
|
178
|
+
else
|
|
179
|
+
response.outcome.reason # => :invalid_structured_output
|
|
180
|
+
response.outcome.detail # => "score is required"
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
171
184
|
#### Nested Objects
|
|
172
185
|
|
|
173
186
|
Use `Hash` with a block to define nested object schemas:
|
data/docs/AGENT_LIFECYCLE.md
CHANGED
|
@@ -30,9 +30,8 @@ agent.generate(prompt = nil, files: nil)
|
|
|
30
30
|
```ruby
|
|
31
31
|
# New conversation (class method — recommended for simple calls)
|
|
32
32
|
response = MyAgent.generate('Hello', context: {user_id: 123})
|
|
33
|
-
puts response.content
|
|
34
|
-
puts response.
|
|
35
|
-
puts response.interrupted? # Check if a callback interrupted the loop
|
|
33
|
+
puts response.content # Access the response text
|
|
34
|
+
puts response.outcome.reason # How the run ended (:completed, :guardrail_blocked, :interrupted, ...)
|
|
36
35
|
|
|
37
36
|
# New conversation (instance method — when you need message history or callbacks)
|
|
38
37
|
agent = MyAgent.new(context: {user_id: 123})
|
|
@@ -142,9 +141,9 @@ Works with both `generate` and `stream`. Only emits agent-generated messages (As
|
|
|
142
141
|
|
|
143
142
|
Callbacks can interrupt the agent loop. This is useful for human-in-the-loop approval, cost limits, or content filtering.
|
|
144
143
|
|
|
145
|
-
Use `agent.interrupt!` (or the lower-level `throw :riffer_interrupt`) to stop the loop. The response will
|
|
144
|
+
Use `agent.interrupt!` (or the lower-level `throw :riffer_interrupt`) to stop the loop. The response's `outcome.reason` will be `:interrupted` and `content` will hold the accumulated content up to the point of interruption.
|
|
146
145
|
|
|
147
|
-
An optional reason can be passed to `interrupt!`. It is available via `
|
|
146
|
+
An optional reason can be passed to `interrupt!`. It is available via `outcome.detail` on the response (generate) or `reason` on the `Interrupt` event (stream):
|
|
148
147
|
|
|
149
148
|
```ruby
|
|
150
149
|
agent = MyAgent.new
|
|
@@ -155,9 +154,9 @@ agent.session.on_message do |msg|
|
|
|
155
154
|
end
|
|
156
155
|
|
|
157
156
|
response = agent.generate('Call the tool')
|
|
158
|
-
response.
|
|
159
|
-
response.
|
|
160
|
-
response.content
|
|
157
|
+
response.outcome.reason # => :interrupted
|
|
158
|
+
response.outcome.detail # => "needs human approval"
|
|
159
|
+
response.content # => last assistant content before interrupt
|
|
161
160
|
```
|
|
162
161
|
|
|
163
162
|
**Streaming** — interrupts emit an `Interrupt` event:
|
|
@@ -188,7 +187,7 @@ agent.session.on_message { |msg| throw :riffer_interrupt if needs_approval?(msg)
|
|
|
188
187
|
|
|
189
188
|
response = agent.generate('Do something risky')
|
|
190
189
|
|
|
191
|
-
if response.interrupted
|
|
190
|
+
if response.outcome.reason == :interrupted
|
|
192
191
|
approve_action(agent.session.messages)
|
|
193
192
|
response = agent.generate('Approved, go ahead') # executes pending tools, then calls the LLM
|
|
194
193
|
# or: agent.generate # resume without a new turn
|
|
@@ -301,23 +300,49 @@ agent.context[:skills] # the Skills::Context, if skills configured
|
|
|
301
300
|
|
|
302
301
|
## Response Attributes
|
|
303
302
|
|
|
304
|
-
`Riffer::Agent::Response` is returned by `generate`:
|
|
303
|
+
`Riffer::Agent::Response` is returned by `generate`. Start with `response.outcome`: its `reason` says how the run ended, and everything else on the response is detail for that reason. `response.content` and `response.structured_output` are only meaningful when the reason is `:completed`; see [response.outcome](#responseoutcome) for the full vocabulary.
|
|
305
304
|
|
|
306
305
|
| Attribute | Type | Description |
|
|
307
306
|
| ---------------------- | --------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
308
307
|
| `content` | `String` | The response text |
|
|
308
|
+
| `outcome` | `Outcome` | How the run ended — `reason` and optional `detail` (see below) |
|
|
309
309
|
| `structured_output` | `Hash` / `nil` | Parsed and validated structured output (see below) |
|
|
310
|
-
| `blocked?` | `Boolean` | `true` if a guardrail tripwire fired |
|
|
311
310
|
| `tripwire` | `Tripwire` / `nil` | The guardrail tripwire that blocked the request |
|
|
312
311
|
| `modified?` | `Boolean` | `true` if a guardrail modified the content |
|
|
313
312
|
| `modifications` | `Array` | List of guardrail modifications applied |
|
|
314
|
-
| `interrupted?` | `Boolean` | `true` if the loop was interrupted |
|
|
315
|
-
| `interrupt_reason` | `String` / `Symbol` / `nil` | The reason passed to `throw :riffer_interrupt` |
|
|
316
313
|
| `messages` | `Array` | Full message history from the conversation |
|
|
317
314
|
| `healed_tool_call_ids` | `Array[String]` | `tool_call` ids filled with placeholder results during interrupt healing (else `[]`) |
|
|
318
315
|
| `token_usage` | `TokenUsage` / `nil` | Aggregate `Riffer::Providers::TokenUsage` across this run's LLM calls (`nil` when none reported) |
|
|
319
316
|
| `steps` | `Integer` | LLM calls made during this run (`0` when a before-guardrail blocks first); not the session's cumulative count |
|
|
320
317
|
|
|
318
|
+
### response.outcome
|
|
319
|
+
|
|
320
|
+
`response.outcome` is a `Riffer::Agent::Outcome` — the single place to read how the run ended. `reason` is always one of the values below; `detail` is a `String` with the specifics when there are any, else `nil`. `outcome.success?` is shorthand for `reason == :completed`.
|
|
321
|
+
|
|
322
|
+
| Reason | Source | `detail` |
|
|
323
|
+
| ---------------------------- | ---------------------------------------------------------- | ----------------------------------------- |
|
|
324
|
+
| `:completed` | The loop ended normally | `nil` |
|
|
325
|
+
| `:guardrail_blocked` | A guardrail tripwire fired (`tripwire` is set) | The tripwire reason |
|
|
326
|
+
| `:max_steps` | The `max_steps` limit was reached | `nil` |
|
|
327
|
+
| `:interrupted` | A callback called `interrupt!` / `throw :riffer_interrupt` | The interrupt reason, or `nil` |
|
|
328
|
+
| `:length`, `:content_filter`, `:context_window`, `:malformed_output`, `:error`, `:other` | The assistant message's normalized `finish_reason` (see [Messages — Finish Reasons](MESSAGES.md#finish-reasons)) | The provider's raw finish value (`finish_reason_raw`), or `nil` |
|
|
329
|
+
| `:invalid_structured_output` | The final message failed JSON parsing or schema validation | The parse or validation error |
|
|
330
|
+
|
|
331
|
+
When several apply, the most causal wins: a guardrail block outranks an interrupt, an interrupt outranks the provider's finish reason, and the provider's finish reason outranks a structured output failure. A run that hit `:length` and therefore produced invalid JSON reports `:length`.
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
agent = MyAgent.new
|
|
335
|
+
response = agent.generate('Hello')
|
|
336
|
+
|
|
337
|
+
case response.outcome.reason
|
|
338
|
+
when :completed then puts response.content
|
|
339
|
+
when :guardrail_blocked then puts "Blocked: #{response.outcome.detail}"
|
|
340
|
+
when :interrupted, :max_steps then response = agent.generate('Continue')
|
|
341
|
+
when :invalid_structured_output then warn response.outcome.detail
|
|
342
|
+
else warn "Provider stopped early: #{response.outcome.reason}"
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
321
346
|
### response.structured_output
|
|
322
347
|
|
|
323
348
|
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`:
|
|
@@ -328,7 +353,7 @@ response.content # => raw JSON string from the LLM
|
|
|
328
353
|
response.structured_output # => {sentiment: "positive", score: 0.95}
|
|
329
354
|
```
|
|
330
355
|
|
|
331
|
-
Returns `nil` when structured output is not configured or when validation fails.
|
|
356
|
+
Returns `nil` when structured output is not configured, or when parsing or validation fails — in which case `response.outcome.reason` is `:invalid_structured_output` and `response.outcome.detail` carries the error.
|
|
332
357
|
|
|
333
358
|
The assistant message in the message history stores the parsed hash, so you can access structured output directly from persisted messages:
|
|
334
359
|
|
data/docs/AGENT_LOOP.md
CHANGED
|
@@ -34,7 +34,7 @@ The agent loop normally runs until the LLM produces a response with no tool call
|
|
|
34
34
|
Guardrails are registered at class definition time and run automatically on every request. When a guardrail calls `block`, it sets a **tripwire** that stops the loop immediately. The LLM is never called (for `:before` guardrails) or its response is discarded (for `:after` guardrails).
|
|
35
35
|
|
|
36
36
|
- **When to use:** Policy enforcement that should always apply — content filtering, input validation, length limits.
|
|
37
|
-
- **Response:** `response.
|
|
37
|
+
- **Response:** `response.outcome.reason` is `:guardrail_blocked`, `response.tripwire` contains the reason and metadata.
|
|
38
38
|
- **Streaming:** Yields a `GuardrailTripwire` event.
|
|
39
39
|
- **Resumable:** No. A tripwire is a hard stop. The caller must change the input and start a new `generate`/`stream` call.
|
|
40
40
|
|
|
@@ -45,7 +45,7 @@ class MyAgent < Riffer::Agent
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
response = MyAgent.generate('blocked input')
|
|
48
|
-
response.
|
|
48
|
+
response.outcome.reason # => :guardrail_blocked
|
|
49
49
|
response.tripwire.reason # => "Content policy violation"
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -54,7 +54,7 @@ response.tripwire.reason # => "Content policy violation"
|
|
|
54
54
|
Callbacks registered with `on_message` can call `agent.interrupt!` (or `throw :riffer_interrupt`) to pause the loop at any point — after receiving an assistant message, after a tool result, etc. The caller controls exactly when and why to interrupt.
|
|
55
55
|
|
|
56
56
|
- **When to use:** Flow control that depends on runtime decisions — human-in-the-loop approval, budget tracking, conditional pausing.
|
|
57
|
-
- **Response:** `response.
|
|
57
|
+
- **Response:** `response.outcome.reason` is `:interrupted`, `response.outcome.detail` contains the optional reason.
|
|
58
58
|
- **Streaming:** Yields an `Interrupt` event with a `reason` attribute.
|
|
59
59
|
- **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.
|
|
60
60
|
|
|
@@ -65,8 +65,8 @@ agent.session.on_message do |msg|
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
response = agent.generate('Do something risky')
|
|
68
|
-
response.
|
|
69
|
-
response.
|
|
68
|
+
response.outcome.reason # => :interrupted
|
|
69
|
+
response.outcome.detail # => "approval needed"
|
|
70
70
|
response = agent.generate('Approved, continue') # continues where it left off
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -75,7 +75,7 @@ response = agent.generate('Approved, continue') # continues where it left off
|
|
|
75
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`.
|
|
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
|
-
- **Response:** `response.
|
|
78
|
+
- **Response:** `response.outcome.reason` is `:max_steps`.
|
|
79
79
|
- **Streaming:** Yields an `Interrupt` event with `reason: :max_steps`.
|
|
80
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.
|
|
81
81
|
|
|
@@ -86,8 +86,7 @@ class MyAgent < Riffer::Agent
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
response = MyAgent.generate('Do a complex task')
|
|
89
|
-
response.
|
|
90
|
-
response.interrupt_reason # => :max_steps
|
|
89
|
+
response.outcome.reason # => :max_steps (if 8 steps were reached)
|
|
91
90
|
```
|
|
92
91
|
|
|
93
92
|
### Unhandled Exceptions
|
|
@@ -101,6 +100,6 @@ If a guardrail, provider call, or other internal code raises an exception, it pr
|
|
|
101
100
|
| Defined | At class level (`guardrail :before`) | At instance level (`on_message`) | At class level (`max_steps 8`) |
|
|
102
101
|
| Fires | Automatically on every request | When callback logic decides | When step count reaches limit |
|
|
103
102
|
| Resumable | No | Yes (call `generate`/`stream` again) | Yes (call `generate`/`stream` again) |
|
|
104
|
-
|
|
|
103
|
+
| Outcome reason | `:guardrail_blocked` | `:interrupted` | `:max_steps` |
|
|
105
104
|
| Stream event | `GuardrailTripwire` | `Interrupt` | `Interrupt` |
|
|
106
105
|
| Purpose | Policy enforcement | Flow control | Runaway loop prevention |
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -198,19 +198,19 @@ Per file, riffer applies this policy, in order:
|
|
|
198
198
|
|
|
199
199
|
1. **Already inline data** — nothing to download. If `sha256:` was given, it's verified against the existing bytes regardless of any other setting below.
|
|
200
200
|
2. **Provider can't accept the file at all** — raises `Riffer::FileUnsupportedError`.
|
|
201
|
-
3. **Provider accepts a URL as-is** — passed straight through, untouched,
|
|
201
|
+
3. **Provider accepts a URL as-is** — passed straight through, untouched, _unless_ `sha256:` was given, in which case riffer downloads and verifies anyway (a caller who set `sha256:` is asking for integrity verification, not a passthrough).
|
|
202
202
|
4. **Provider needs the bytes inline** — riffer downloads the file, verifying `sha256:` if given.
|
|
203
203
|
|
|
204
204
|
Every download in step 3 or 4 is gated by `allow_downloads`; with it `false` (the default), reaching either of those steps raises `Riffer::FileDownloadsDisabledError` instead of fetching anything. This means upgrading to a riffer version with this feature never starts downloading arbitrary URLs on your behalf — you have to opt in.
|
|
205
205
|
|
|
206
|
-
| Option | Description
|
|
207
|
-
| ----------------- |
|
|
208
|
-
| `allow_downloads` | Whether riffer may download a `FilePart`'s URL. Accepts booleans or `'true'`/`'false'`/`'1'`/`'0'`. Defaults to `false`.
|
|
209
|
-
| `max_bytes` | Maximum size, in bytes, of a downloaded file; the download is aborted once the streamed body exceeds this, independent of (and regardless of a missing/lying) `content-length` header. Defaults to `3_500_000`.
|
|
210
|
-
| `timeout` | Open and read timeout, in seconds, for a single download attempt. Defaults to `60`.
|
|
211
|
-
| `max_per_message` | Maximum number of files allowed on a single user message; checked against each message as originally authored, before consecutive messages are merged. `nil` (default) means uncapped.
|
|
212
|
-
| `runner` | A `Riffer::Runner` instance that resolves every file across a call's messages. Defaults to `Riffer::Runner::Sequential.new`; assign `Riffer::Runner::Threaded.new` (or `Riffer::Runner::Fibers.new` inside a fiber-based host) to resolve multiple files concurrently.
|
|
213
|
-
| `downloader` | The object that fetches a URL's bytes; must respond to `#call(url, max_bytes:, timeout:)` returning the raw (not base64-encoded) file content. Riffer caches it as base64 or raw bytes, whichever the provider actually needs, rather than producing both. Defaults to `Riffer::Files::Downloader.new`, which fetches over HTTPS only, following up to 3 redirects. Assign your own to add logging/metrics, or to fetch from a non-HTTPS store (e.g. `s3://`).
|
|
206
|
+
| Option | Description |
|
|
207
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
208
|
+
| `allow_downloads` | Whether riffer may download a `FilePart`'s URL. Accepts booleans or `'true'`/`'false'`/`'1'`/`'0'`. Defaults to `false`. |
|
|
209
|
+
| `max_bytes` | Maximum size, in bytes, of a downloaded file; the download is aborted once the streamed body exceeds this, independent of (and regardless of a missing/lying) `content-length` header. Defaults to `3_500_000`. |
|
|
210
|
+
| `timeout` | Open and read timeout, in seconds, for a single download attempt. Defaults to `60`. |
|
|
211
|
+
| `max_per_message` | Maximum number of files allowed on a single user message; checked against each message as originally authored, before consecutive messages are merged. `nil` (default) means uncapped. |
|
|
212
|
+
| `runner` | A `Riffer::Runner` instance that resolves every file across a call's messages. Defaults to `Riffer::Runner::Sequential.new`; assign `Riffer::Runner::Threaded.new` (or `Riffer::Runner::Fibers.new` inside a fiber-based host) to resolve multiple files concurrently. |
|
|
213
|
+
| `downloader` | The object that fetches a URL's bytes; must respond to `#call(url, max_bytes:, timeout:)` returning the raw (not base64-encoded) file content. Riffer caches it as base64 or raw bytes, whichever the provider actually needs, rather than producing both. Defaults to `Riffer::Files::Downloader.new`, which fetches over HTTPS only, following up to 3 redirects. Assign your own to add logging/metrics, or to fetch from a non-HTTPS store (e.g. `s3://`). |
|
|
214
214
|
|
|
215
215
|
A file that fails resolution raises a `Riffer::FileError` subclass — `Riffer::FileUnsupportedError`, `Riffer::FileDownloadsDisabledError`, `Riffer::TooManyFilesError`, `Riffer::FileChecksumMismatchError`, `Riffer::FileTooLargeError`, `Riffer::FileDownloadError`, or `Riffer::FileEncodingError` — so callers can `rescue Riffer::FileError` for any attachment problem, or a specific subclass to handle one case.
|
|
216
216
|
|
|
@@ -345,14 +345,15 @@ end
|
|
|
345
345
|
|
|
346
346
|
### Anthropic
|
|
347
347
|
|
|
348
|
-
| Option
|
|
349
|
-
|
|
|
350
|
-
| `temperature`
|
|
351
|
-
| `max_tokens`
|
|
352
|
-
| `top_p`
|
|
353
|
-
| `top_k`
|
|
354
|
-
| `thinking`
|
|
355
|
-
| `
|
|
348
|
+
| Option | Description |
|
|
349
|
+
| --------------- | ------------------------------------------- |
|
|
350
|
+
| `temperature` | Sampling temperature |
|
|
351
|
+
| `max_tokens` | Maximum tokens in response |
|
|
352
|
+
| `top_p` | Nucleus sampling parameter |
|
|
353
|
+
| `top_k` | Top-k sampling parameter |
|
|
354
|
+
| `thinking` | Extended thinking config hash (Claude 3.7+) |
|
|
355
|
+
| `output_config` | Output config hash (e.g. `effort`) |
|
|
356
|
+
| `web_search` | Enable web search (`true` or config hash) |
|
|
356
357
|
|
|
357
358
|
```ruby
|
|
358
359
|
class MyAgent < Riffer::Agent
|
|
@@ -365,6 +366,12 @@ class ReasoningAgent < Riffer::Agent
|
|
|
365
366
|
model 'anthropic/claude-haiku-4-5-20251001'
|
|
366
367
|
model_options thinking: {type: "enabled", budget_tokens: 10000}
|
|
367
368
|
end
|
|
369
|
+
|
|
370
|
+
# With an output effort level
|
|
371
|
+
class EffortAgent < Riffer::Agent
|
|
372
|
+
model 'anthropic/claude-opus-5'
|
|
373
|
+
model_options output_config: {effort: "high"}
|
|
374
|
+
end
|
|
368
375
|
```
|
|
369
376
|
|
|
370
377
|
## Environment Variables
|
data/docs/GUARDRAILS.md
CHANGED
|
@@ -28,10 +28,10 @@ class MyAgent < Riffer::Agent
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
response = MyAgent.generate("Hello!")
|
|
31
|
-
response.
|
|
31
|
+
response.outcome.reason # => :completed
|
|
32
32
|
|
|
33
33
|
response = MyAgent.generate("You are a badword")
|
|
34
|
-
response.
|
|
34
|
+
response.outcome.reason # => :guardrail_blocked
|
|
35
35
|
response.tripwire.reason # => "Profanity detected"
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -195,7 +195,7 @@ end
|
|
|
195
195
|
response = MyAgent.generate("Hello")
|
|
196
196
|
|
|
197
197
|
response.content # The response text
|
|
198
|
-
response.
|
|
198
|
+
response.outcome.reason # :guardrail_blocked if a guardrail blocked execution
|
|
199
199
|
response.tripwire # Tripwire object with block details (if blocked)
|
|
200
200
|
response.modified? # true if any guardrail transformed data
|
|
201
201
|
response.modifications # Array of Modification records
|
|
@@ -206,7 +206,7 @@ response.modifications # Array of Modification records
|
|
|
206
206
|
```ruby
|
|
207
207
|
response = MyAgent.generate("Hello")
|
|
208
208
|
|
|
209
|
-
if response.
|
|
209
|
+
if response.outcome.reason == :guardrail_blocked
|
|
210
210
|
puts "Blocked: #{response.tripwire.reason}"
|
|
211
211
|
puts "Phase: #{response.tripwire.phase}"
|
|
212
212
|
puts "Guardrail: #{response.tripwire.guardrail}"
|
data/docs/MESSAGES.md
CHANGED
|
@@ -49,7 +49,8 @@ msg.role # => :assistant
|
|
|
49
49
|
msg.content # => "I'm doing well, thank you!"
|
|
50
50
|
msg.tool_calls # => []
|
|
51
51
|
msg.token_usage # => nil or Riffer::Providers::TokenUsage
|
|
52
|
-
msg.finish_reason
|
|
52
|
+
msg.finish_reason # => nil or a normalized Symbol (see below)
|
|
53
|
+
msg.finish_reason_raw # => nil or the provider's raw wire value (e.g. "max_tokens")
|
|
53
54
|
|
|
54
55
|
# Response with tool calls
|
|
55
56
|
msg = Riffer::Messages::Assistant.new("", tool_calls: [
|
|
@@ -94,7 +95,7 @@ The cache buckets are subsets of `input_tokens`, never additions to it — summi
|
|
|
94
95
|
| `:error` | The provider reported an error finish. |
|
|
95
96
|
| `:other` | A provider-specific value with no normalized equivalent. |
|
|
96
97
|
|
|
97
|
-
`finish_reason` is `nil` when the provider doesn't report one. The provider's raw wire value travels alongside on the `FinishReasonDone` stream event and the `riffer.finish_reason.raw` trace attribute — for OpenRouter that is the upstream model's `native_finish_reason`, and for a failed OpenAI response it is the error code. Use `finish_reason` to detect truncation without parsing provider responses:
|
|
98
|
+
`finish_reason` is `nil` when the provider doesn't report one. The provider's raw wire value travels alongside as `finish_reason_raw` on the message (round-tripped through `to_h` / `from_hash`), on the `FinishReasonDone` stream event, and as the `riffer.finish_reason.raw` trace attribute — for OpenRouter that is the upstream model's `native_finish_reason`, and for a failed OpenAI response it is the error code. Use `finish_reason` to detect truncation without parsing provider responses:
|
|
98
99
|
|
|
99
100
|
```ruby
|
|
100
101
|
response = agent.generate("Summarize this document")
|
|
@@ -103,7 +104,7 @@ retry_with_higher_limit if agent.session.messages.last.finish_reason == :length
|
|
|
103
104
|
|
|
104
105
|
#### Structured Output on Messages
|
|
105
106
|
|
|
106
|
-
When an agent has `structured_output` configured, the final assistant message stores the parsed hash directly. The `structured_output?` predicate checks for a non-nil value:
|
|
107
|
+
When an agent has `structured_output` configured, the final assistant message stores the parsed hash directly. The message holds the parsed JSON, not the schema-validated result; schema validation is reported on `response.outcome` (see [Agent Lifecycle — response.outcome](AGENT_LIFECYCLE.md#responseoutcome)). The `structured_output?` predicate checks for a non-nil value:
|
|
107
108
|
|
|
108
109
|
```ruby
|
|
109
110
|
msg = Riffer::Messages::Assistant.new('{"sentiment":"positive"}', structured_output: {sentiment: "positive"})
|
data/docs/STREAM_EVENTS.md
CHANGED
|
@@ -215,7 +215,7 @@ Emitted when the agent loop is interrupted. This can happen in two ways:
|
|
|
215
215
|
- An `on_message` callback calls `agent.interrupt!` or `throw :riffer_interrupt` (reason is a String or `nil`).
|
|
216
216
|
- The `max_steps` limit is reached (reason is the Symbol `:max_steps`).
|
|
217
217
|
|
|
218
|
-
This is the streaming equivalent of `
|
|
218
|
+
This is the streaming equivalent of `response.outcome.reason == :interrupted` (or `:max_steps`) in generate mode.
|
|
219
219
|
|
|
220
220
|
```ruby
|
|
221
221
|
# Callback interrupt with a string reason
|
data/docs/TOOL_ADVANCED.md
CHANGED
|
@@ -220,6 +220,8 @@ end
|
|
|
220
220
|
|
|
221
221
|
Fibers use cooperative scheduling — they yield control at I/O boundaries (network calls, file reads, sleep). CPU-bound tools will not benefit from the fibers runtime. Be mindful of fiber-local state (`Fiber.[]`) and note that `Thread.current[]` values are shared across all fibers in the same thread.
|
|
222
222
|
|
|
223
|
+
The fibers runtime is safe to use inside an existing reactor. When called from plain Ruby it starts its own reactor and blocks until every tool call finishes. When the process is already inside an Async task — for example under Falcon, or inside an `Async do ... end` block — it joins the current task instead of starting a nested reactor, so it is safe to call from request handlers.
|
|
224
|
+
|
|
223
225
|
### Custom Runtimes
|
|
224
226
|
|
|
225
227
|
Create a custom runtime by subclassing `Riffer::Tools::Runtime` and overriding the private `dispatch_tool_call` method:
|
data/docs/TRACING.md
CHANGED
|
@@ -113,6 +113,8 @@ Any tags passed to `#generate` / `#stream` via `tags:` are stamped on **all four
|
|
|
113
113
|
| `gen_ai.usage.cache_read.input_tokens` | int | When the provider reported cache reads |
|
|
114
114
|
| `gen_ai.usage.cache_creation.input_tokens` | int | When the provider reported cache writes |
|
|
115
115
|
| `riffer.cost` | float | When every call in the run was priced |
|
|
116
|
+
| `riffer.outcome.reason` | string | Always — the `response.outcome.reason` |
|
|
117
|
+
| `riffer.outcome.detail` | string | When `response.outcome.detail` is present |
|
|
116
118
|
| `riffer.interrupt.reason` | string | On interrupt (e.g. approval needed, max steps) |
|
|
117
119
|
| `riffer.tripwire.guardrail` | string | On a guardrail tripwire, when the guardrail is named |
|
|
118
120
|
| `riffer.tripwire.reason` | string | On a guardrail tripwire |
|
|
@@ -271,7 +273,7 @@ When enabled, content is serialized as GenAI-semconv JSON strings. File attachme
|
|
|
271
273
|
The span and attribute shape is a public, versioned contract, in two tiers:
|
|
272
274
|
|
|
273
275
|
- **`gen_ai.*`** tracks the OpenTelemetry GenAI semantic conventions, pinned to schema version `1.37.0`. That convention is still "Development" status upstream and its attribute names may change; Riffer absorbs such renames deliberately in a release, never silently, with a CHANGELOG entry.
|
|
274
|
-
- **`riffer.*`** is Riffer-owned (`riffer.steps`, `riffer.cost`, `riffer.interrupt.reason`, `riffer.tripwire.*`, `riffer.guardrail.*`, `riffer.finish_reason.raw`) and changes only through a normal version bump and CHANGELOG entry.
|
|
276
|
+
- **`riffer.*`** is Riffer-owned (`riffer.steps`, `riffer.cost`, `riffer.outcome.*`, `riffer.interrupt.reason`, `riffer.tripwire.*`, `riffer.guardrail.*`, `riffer.finish_reason.raw`) and changes only through a normal version bump and CHANGELOG entry.
|
|
275
277
|
|
|
276
278
|
The semantic-convention schema version is a documented pin rather than a span attribute — the OpenTelemetry Ruby API can't attach a schema URL to a tracer. The runtime version signal is the instrumentation scope: every span carries scope name `riffer` at the gem version that emitted it. Pin the Riffer version your dashboards depend on, and watch the CHANGELOG for tracing entries before upgrading.
|
|
277
279
|
|
|
@@ -21,13 +21,14 @@ Use `stub_response` to queue responses:
|
|
|
21
21
|
```ruby
|
|
22
22
|
# Get the provider instance from the agent
|
|
23
23
|
agent = TestableAgent.new
|
|
24
|
-
provider = agent.
|
|
24
|
+
provider = agent.provider
|
|
25
25
|
|
|
26
26
|
# Stub a simple text response
|
|
27
27
|
provider.stub_response("Hello, I'm here to help!")
|
|
28
28
|
|
|
29
29
|
# Now generate will return the stubbed response
|
|
30
30
|
response = agent.generate("Hi")
|
|
31
|
+
response.content
|
|
31
32
|
# => "Hello, I'm here to help!"
|
|
32
33
|
```
|
|
33
34
|
|
|
@@ -55,10 +56,10 @@ provider.stub_response("First response")
|
|
|
55
56
|
provider.stub_response("Second response")
|
|
56
57
|
provider.stub_response("Third response")
|
|
57
58
|
|
|
58
|
-
agent.generate("Message 1") # => "First response"
|
|
59
|
-
agent.generate("Message 2") # => "Second response"
|
|
60
|
-
agent.generate("Message 3") # => "Third response"
|
|
61
|
-
agent.generate("Message 4") # => "Mock response" (default)
|
|
59
|
+
agent.generate("Message 1").content # => "First response"
|
|
60
|
+
agent.generate("Message 2").content # => "Second response"
|
|
61
|
+
agent.generate("Message 3").content # => "Third response"
|
|
62
|
+
agent.generate("Message 4").content # => "Mock response" (default)
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
## Inspecting Calls
|
|
@@ -92,7 +93,7 @@ require 'minitest/autorun'
|
|
|
92
93
|
class MyAgentTest < Minitest::Test
|
|
93
94
|
def setup
|
|
94
95
|
@agent = TestableAgent.new
|
|
95
|
-
@provider = @agent.
|
|
96
|
+
@provider = @agent.provider
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
def test_generates_response
|
|
@@ -146,18 +147,24 @@ text_done = events.find { |e| e.is_a?(Riffer::StreamEvents::TextDone) }
|
|
|
146
147
|
|
|
147
148
|
## Web Search
|
|
148
149
|
|
|
149
|
-
The
|
|
150
|
+
The mock provider emits web search events when `web_search: true` is set in the agent's `model_options`:
|
|
150
151
|
|
|
151
152
|
```ruby
|
|
152
|
-
|
|
153
|
+
class SearchingAgent < Riffer::Agent
|
|
154
|
+
model 'mock/any'
|
|
155
|
+
model_options web_search: true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
agent = SearchingAgent.new
|
|
159
|
+
agent.provider.stub_response("Here are the latest results.")
|
|
153
160
|
|
|
154
161
|
events = []
|
|
155
|
-
agent.stream("What's new in Ruby?"
|
|
162
|
+
agent.stream("What's new in Ruby?").each { |e| events << e }
|
|
156
163
|
|
|
157
164
|
# Events include WebSearchStatus and WebSearchDone before text events
|
|
158
|
-
|
|
165
|
+
search_statuses = events.select { |e| e.is_a?(Riffer::StreamEvents::WebSearchStatus) }
|
|
159
166
|
search_done = events.find { |e| e.is_a?(Riffer::StreamEvents::WebSearchDone) }
|
|
160
|
-
search_done.query # => "
|
|
167
|
+
search_done.query # => "mock search query"
|
|
161
168
|
search_done.sources # => [{title: "Example", url: "https://example.com"}]
|
|
162
169
|
```
|
|
163
170
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# How a run ended — the single place to read whether the agent completed
|
|
5
|
+
# normally and, if not, why. +detail+ carries the specifics when there are any:
|
|
6
|
+
# the tripwire reason, the interrupt reason, the provider's raw finish value,
|
|
7
|
+
# or the structured output parse/validation error.
|
|
8
|
+
#
|
|
9
|
+
# response = agent.generate("Analyze this")
|
|
10
|
+
# case response.outcome.reason
|
|
11
|
+
# when :completed then puts response.structured_output
|
|
12
|
+
# when :invalid_structured_output then warn response.outcome.detail
|
|
13
|
+
# end
|
|
14
|
+
class Riffer::Agent::Outcome
|
|
15
|
+
# Finish reasons that end a turn normally; every other finish reason means the
|
|
16
|
+
# provider cut the turn short and surfaces as the run's outcome verbatim.
|
|
17
|
+
NORMAL_FINISH_REASONS = %i[stop tool_calls].freeze #: Array[Symbol]
|
|
18
|
+
|
|
19
|
+
# Derived from the provider vocabulary so a new finish reason becomes an
|
|
20
|
+
# outcome without a second list to update.
|
|
21
|
+
PROVIDER_STOP_REASONS = (Riffer::Providers::FinishReason::VALUES - NORMAL_FINISH_REASONS).freeze #: Array[Symbol]
|
|
22
|
+
|
|
23
|
+
# The vocabulary every run ends in.
|
|
24
|
+
VALUES = (%i[completed guardrail_blocked interrupted max_steps invalid_structured_output] +
|
|
25
|
+
PROVIDER_STOP_REASONS).freeze #: Array[Symbol]
|
|
26
|
+
|
|
27
|
+
# Why the run ended.
|
|
28
|
+
attr_reader :reason #: Symbol
|
|
29
|
+
|
|
30
|
+
# Human-readable specifics for +reason+, when there are any.
|
|
31
|
+
attr_reader :detail #: String?
|
|
32
|
+
|
|
33
|
+
# Raises Riffer::ArgumentError when +reason+ is outside VALUES.
|
|
34
|
+
#--
|
|
35
|
+
#: (reason: Symbol, ?detail: String?) -> void
|
|
36
|
+
def initialize(reason:, detail: nil)
|
|
37
|
+
unless VALUES.include?(reason)
|
|
38
|
+
raise Riffer::ArgumentError, "reason must be one of #{VALUES.inspect}, got #{reason.inspect}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@reason = reason
|
|
42
|
+
@detail = detail
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns true when the run completed normally.
|
|
46
|
+
#
|
|
47
|
+
#--
|
|
48
|
+
#: () -> bool
|
|
49
|
+
def success?
|
|
50
|
+
reason == :completed
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Wraps an agent generation response.
|
|
5
|
-
# +content+ is empty and +tripwire+ carries the
|
|
4
|
+
# Wraps an agent generation response. +outcome+ says how the run ended; when a
|
|
5
|
+
# guardrail blocks execution, +content+ is empty and +tripwire+ carries the
|
|
6
|
+
# block details.
|
|
6
7
|
#
|
|
7
8
|
# response = agent.generate("Hello")
|
|
8
|
-
# if response.
|
|
9
|
-
# puts "Blocked: #{response.tripwire.reason}"
|
|
10
|
-
# else
|
|
9
|
+
# if response.outcome.success?
|
|
11
10
|
# puts response.content
|
|
11
|
+
# else
|
|
12
|
+
# puts "#{response.outcome.reason}: #{response.outcome.detail}"
|
|
12
13
|
# end
|
|
13
14
|
class Riffer::Agent::Response
|
|
14
|
-
# @rbs @interrupted: bool
|
|
15
|
-
|
|
16
15
|
# The response content.
|
|
17
16
|
attr_reader :content #: String
|
|
18
17
|
|
|
18
|
+
# How the run ended.
|
|
19
|
+
attr_reader :outcome #: Riffer::Agent::Outcome
|
|
20
|
+
|
|
19
21
|
# The tripwire if execution was blocked.
|
|
20
22
|
attr_reader :tripwire #: Riffer::Guardrails::Tripwire?
|
|
21
23
|
|
|
22
24
|
# The modifications made by guardrails during processing.
|
|
23
25
|
attr_reader :modifications #: Array[Riffer::Guardrails::Modification]
|
|
24
26
|
|
|
25
|
-
# The reason provided with the interrupt, if any.
|
|
26
|
-
attr_reader :interrupt_reason #: (String | Symbol)?
|
|
27
|
-
|
|
28
27
|
# The parsed structured output, if structured output was configured.
|
|
29
28
|
attr_reader :structured_output #: Hash[Symbol, untyped]?
|
|
30
29
|
|
|
@@ -45,10 +44,9 @@ class Riffer::Agent::Response
|
|
|
45
44
|
#--
|
|
46
45
|
#: (
|
|
47
46
|
# String,
|
|
47
|
+
# outcome: Riffer::Agent::Outcome,
|
|
48
48
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
49
49
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
50
|
-
# ?interrupted: bool,
|
|
51
|
-
# ?interrupt_reason: (String | Symbol)?,
|
|
52
50
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
53
51
|
# ?messages: Array[Riffer::Messages::Base],
|
|
54
52
|
# ?healed_tool_call_ids: Array[String],
|
|
@@ -57,10 +55,9 @@ class Riffer::Agent::Response
|
|
|
57
55
|
# ) -> void
|
|
58
56
|
def initialize(
|
|
59
57
|
content,
|
|
58
|
+
outcome:,
|
|
60
59
|
tripwire: nil,
|
|
61
60
|
modifications: [],
|
|
62
|
-
interrupted: false,
|
|
63
|
-
interrupt_reason: nil,
|
|
64
61
|
structured_output: nil,
|
|
65
62
|
messages: [],
|
|
66
63
|
healed_tool_call_ids: [],
|
|
@@ -68,10 +65,9 @@ class Riffer::Agent::Response
|
|
|
68
65
|
steps: 0
|
|
69
66
|
)
|
|
70
67
|
@content = content
|
|
68
|
+
@outcome = outcome
|
|
71
69
|
@tripwire = tripwire
|
|
72
70
|
@modifications = modifications
|
|
73
|
-
@interrupted = interrupted
|
|
74
|
-
@interrupt_reason = interrupt_reason
|
|
75
71
|
@structured_output = structured_output
|
|
76
72
|
@messages = messages
|
|
77
73
|
@healed_tool_call_ids = healed_tool_call_ids
|
|
@@ -79,14 +75,6 @@ class Riffer::Agent::Response
|
|
|
79
75
|
@steps = steps
|
|
80
76
|
end
|
|
81
77
|
|
|
82
|
-
# Returns true if the response was blocked by a guardrail.
|
|
83
|
-
#
|
|
84
|
-
#--
|
|
85
|
-
#: () -> bool
|
|
86
|
-
def blocked?
|
|
87
|
-
!tripwire.nil?
|
|
88
|
-
end
|
|
89
|
-
|
|
90
78
|
# Returns true if any guardrail modified data during processing.
|
|
91
79
|
#
|
|
92
80
|
#--
|
|
@@ -94,13 +82,4 @@ class Riffer::Agent::Response
|
|
|
94
82
|
def modified?
|
|
95
83
|
modifications.any?
|
|
96
84
|
end
|
|
97
|
-
|
|
98
|
-
# Returns true if the agent loop was interrupted by a callback
|
|
99
|
-
# via <tt>throw :riffer_interrupt</tt>.
|
|
100
|
-
#
|
|
101
|
-
#--
|
|
102
|
-
#: () -> bool
|
|
103
|
-
def interrupted?
|
|
104
|
-
@interrupted
|
|
105
|
-
end
|
|
106
85
|
end
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -155,6 +155,7 @@ module Riffer::Agent::Run
|
|
|
155
155
|
accumulated_tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
156
156
|
accumulated_token_usage = nil #: Riffer::Providers::TokenUsage?
|
|
157
157
|
accumulated_finish_reason = nil #: Symbol?
|
|
158
|
+
accumulated_finish_reason_raw = nil #: String?
|
|
158
159
|
|
|
159
160
|
call_llm_stream(agent, tags).each do |event|
|
|
160
161
|
stream_yielder << event
|
|
@@ -178,6 +179,7 @@ module Riffer::Agent::Run
|
|
|
178
179
|
accumulated_token_usage = event.token_usage
|
|
179
180
|
when Riffer::StreamEvents::FinishReasonDone
|
|
180
181
|
accumulated_finish_reason = event.finish_reason
|
|
182
|
+
accumulated_finish_reason_raw = event.raw_finish_reason
|
|
181
183
|
end
|
|
182
184
|
end
|
|
183
185
|
|
|
@@ -186,6 +188,7 @@ module Riffer::Agent::Run
|
|
|
186
188
|
tool_calls: accumulated_tool_calls,
|
|
187
189
|
token_usage: accumulated_token_usage,
|
|
188
190
|
finish_reason: accumulated_finish_reason,
|
|
191
|
+
finish_reason_raw: accumulated_finish_reason_raw,
|
|
189
192
|
)
|
|
190
193
|
end
|
|
191
194
|
|
|
@@ -207,6 +210,7 @@ module Riffer::Agent::Run
|
|
|
207
210
|
build_response(
|
|
208
211
|
agent,
|
|
209
212
|
"",
|
|
213
|
+
outcome: Riffer::Agent::Outcome.new(reason: :guardrail_blocked, detail: tripwire.reason),
|
|
210
214
|
tripwire: tripwire,
|
|
211
215
|
modifications: all_modifications,
|
|
212
216
|
token_usage: token_usage,
|
|
@@ -215,18 +219,41 @@ module Riffer::Agent::Run
|
|
|
215
219
|
end
|
|
216
220
|
|
|
217
221
|
#--
|
|
218
|
-
#: (Riffer::Agent, Array[Riffer::Guardrails::Modification], **untyped) -> Riffer::Agent::Response
|
|
219
|
-
def final_response(agent, all_modifications, **extra)
|
|
220
|
-
|
|
222
|
+
#: (Riffer::Agent, Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, **untyped) -> Riffer::Agent::Response
|
|
223
|
+
def final_response(agent, all_modifications, interrupted: false, interrupt_reason: nil, **extra)
|
|
224
|
+
message = agent.session.final_assistant_message
|
|
225
|
+
result = agent.structured_output && structured_output_result(agent, message)
|
|
221
226
|
build_response(
|
|
222
227
|
agent,
|
|
223
|
-
|
|
228
|
+
message&.content || "",
|
|
229
|
+
outcome: final_outcome(message, result, interrupted: interrupted, interrupt_reason: interrupt_reason),
|
|
224
230
|
modifications: all_modifications,
|
|
225
|
-
structured_output:
|
|
231
|
+
structured_output: result&.object,
|
|
226
232
|
**extra,
|
|
227
233
|
)
|
|
228
234
|
end
|
|
229
235
|
|
|
236
|
+
# Checked in the order things happened. The loop being stopped (max_steps or
|
|
237
|
+
# an interrupt) beats the provider's finish reason, which beats riffer's own
|
|
238
|
+
# validation of the content. A truncated response that also fails the schema
|
|
239
|
+
# therefore reports :length, not :invalid_structured_output.
|
|
240
|
+
#--
|
|
241
|
+
#: (Riffer::Messages::Assistant?, Riffer::Agent::StructuredOutput::Result?, interrupted: bool, interrupt_reason: (String | Symbol)?) -> Riffer::Agent::Outcome
|
|
242
|
+
def final_outcome(message, result, interrupted:, interrupt_reason:)
|
|
243
|
+
finish_reason = message&.finish_reason
|
|
244
|
+
if interrupted && interrupt_reason == Riffer::Agent::INTERRUPT_MAX_STEPS
|
|
245
|
+
Riffer::Agent::Outcome.new(reason: :max_steps)
|
|
246
|
+
elsif interrupted
|
|
247
|
+
Riffer::Agent::Outcome.new(reason: :interrupted, detail: interrupt_reason&.to_s)
|
|
248
|
+
elsif finish_reason && !Riffer::Agent::Outcome::NORMAL_FINISH_REASONS.include?(finish_reason)
|
|
249
|
+
Riffer::Agent::Outcome.new(reason: finish_reason, detail: message&.finish_reason_raw)
|
|
250
|
+
elsif result&.failure?
|
|
251
|
+
Riffer::Agent::Outcome.new(reason: :invalid_structured_output, detail: result.error)
|
|
252
|
+
else
|
|
253
|
+
Riffer::Agent::Outcome.new(reason: :completed)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
230
257
|
#--
|
|
231
258
|
#: (Riffer::Agent, ?Hash[String, String]) -> Riffer::Messages::Assistant
|
|
232
259
|
def call_llm(agent, tags = {})
|
|
@@ -327,11 +354,11 @@ module Riffer::Agent::Run
|
|
|
327
354
|
end
|
|
328
355
|
|
|
329
356
|
#--
|
|
330
|
-
#: (Riffer::Agent, Riffer::Messages::Assistant?) ->
|
|
331
|
-
def
|
|
332
|
-
return unless
|
|
357
|
+
#: (Riffer::Agent, Riffer::Messages::Assistant?) -> Riffer::Agent::StructuredOutput::Result?
|
|
358
|
+
def structured_output_result(agent, message)
|
|
359
|
+
return unless message
|
|
333
360
|
|
|
334
|
-
agent.structured_output
|
|
361
|
+
agent.structured_output&.parse_and_validate(message.content)
|
|
335
362
|
end
|
|
336
363
|
|
|
337
364
|
#--
|
|
@@ -358,10 +385,9 @@ module Riffer::Agent::Run
|
|
|
358
385
|
#: (
|
|
359
386
|
# Riffer::Agent,
|
|
360
387
|
# String,
|
|
388
|
+
# outcome: Riffer::Agent::Outcome,
|
|
361
389
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
362
390
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
363
|
-
# ?interrupted: bool,
|
|
364
|
-
# ?interrupt_reason: (String | Symbol)?,
|
|
365
391
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
366
392
|
# ?healed_tool_call_ids: Array[String],
|
|
367
393
|
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
@@ -370,10 +396,9 @@ module Riffer::Agent::Run
|
|
|
370
396
|
def build_response(
|
|
371
397
|
agent,
|
|
372
398
|
content,
|
|
399
|
+
outcome:,
|
|
373
400
|
tripwire: nil,
|
|
374
401
|
modifications: [],
|
|
375
|
-
interrupted: false,
|
|
376
|
-
interrupt_reason: nil,
|
|
377
402
|
structured_output: nil,
|
|
378
403
|
healed_tool_call_ids: [],
|
|
379
404
|
token_usage: nil,
|
|
@@ -382,10 +407,9 @@ module Riffer::Agent::Run
|
|
|
382
407
|
messages = agent.session.messages
|
|
383
408
|
Riffer::Agent::Response.new(
|
|
384
409
|
content,
|
|
410
|
+
outcome: outcome,
|
|
385
411
|
tripwire: tripwire,
|
|
386
412
|
modifications: modifications,
|
|
387
|
-
interrupted: interrupted,
|
|
388
|
-
interrupt_reason: interrupt_reason,
|
|
389
413
|
structured_output: structured_output,
|
|
390
414
|
messages: messages.frozen? ? messages : messages.dup.freeze,
|
|
391
415
|
healed_tool_call_ids: healed_tool_call_ids,
|
|
@@ -460,7 +484,12 @@ module Riffer::Agent::Run
|
|
|
460
484
|
span.set_attribute("riffer.steps", response.steps)
|
|
461
485
|
Riffer::Tracing.record_usage(span, response.token_usage)
|
|
462
486
|
|
|
463
|
-
|
|
487
|
+
outcome = response.outcome
|
|
488
|
+
span.set_attribute("riffer.outcome.reason", outcome.reason.to_s)
|
|
489
|
+
detail = outcome.detail
|
|
490
|
+
span.set_attribute("riffer.outcome.detail", detail) if detail
|
|
491
|
+
interrupt_reason = interrupt_reason_attribute(outcome)
|
|
492
|
+
span.set_attribute("riffer.interrupt.reason", interrupt_reason) if interrupt_reason
|
|
464
493
|
|
|
465
494
|
tripwire = response.tripwire
|
|
466
495
|
return unless tripwire
|
|
@@ -470,4 +499,13 @@ module Riffer::Agent::Run
|
|
|
470
499
|
span.set_attribute("riffer.tripwire.reason", tripwire.reason)
|
|
471
500
|
span.set_attribute("riffer.tripwire.phase", tripwire.phase.to_s)
|
|
472
501
|
end
|
|
502
|
+
|
|
503
|
+
#--
|
|
504
|
+
#: (Riffer::Agent::Outcome) -> String?
|
|
505
|
+
def interrupt_reason_attribute(outcome)
|
|
506
|
+
case outcome.reason
|
|
507
|
+
when :max_steps then Riffer::Agent::INTERRUPT_MAX_STEPS.to_s
|
|
508
|
+
when :interrupted then outcome.detail
|
|
509
|
+
end
|
|
510
|
+
end
|
|
473
511
|
end
|
data/lib/riffer/agent/session.rb
CHANGED
|
@@ -203,6 +203,8 @@ class Riffer::Agent::Session
|
|
|
203
203
|
tool_calls: attrs.fetch(:tool_calls, old.tool_calls),
|
|
204
204
|
token_usage: attrs.fetch(:token_usage, old.token_usage),
|
|
205
205
|
structured_output: attrs.fetch(:structured_output, old.structured_output),
|
|
206
|
+
finish_reason: attrs.fetch(:finish_reason, old.finish_reason),
|
|
207
|
+
finish_reason_raw: attrs.fetch(:finish_reason_raw, old.finish_reason_raw),
|
|
206
208
|
)
|
|
207
209
|
when Riffer::Messages::Tool
|
|
208
210
|
Riffer::Messages::Tool.new(
|
|
@@ -19,11 +19,31 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
19
19
|
# <tt>Riffer::Providers::FinishReason::VALUES</tt>).
|
|
20
20
|
attr_reader :finish_reason #: Symbol?
|
|
21
21
|
|
|
22
|
+
# The provider's raw finish-reason value behind +finish_reason+, when one
|
|
23
|
+
# exists on the wire.
|
|
24
|
+
attr_reader :finish_reason_raw #: String?
|
|
25
|
+
|
|
22
26
|
# Raises Riffer::ArgumentError when +finish_reason+ is outside the
|
|
23
27
|
# normalized vocabulary.
|
|
24
28
|
#--
|
|
25
|
-
#: (
|
|
26
|
-
|
|
29
|
+
#: (
|
|
30
|
+
# String,
|
|
31
|
+
# ?id: String?,
|
|
32
|
+
# ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall],
|
|
33
|
+
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
34
|
+
# ?structured_output: Hash[Symbol, untyped]?,
|
|
35
|
+
# ?finish_reason: Symbol?,
|
|
36
|
+
# ?finish_reason_raw: String?
|
|
37
|
+
# ) -> void
|
|
38
|
+
def initialize(
|
|
39
|
+
content,
|
|
40
|
+
id: nil,
|
|
41
|
+
tool_calls: [],
|
|
42
|
+
token_usage: nil,
|
|
43
|
+
structured_output: nil,
|
|
44
|
+
finish_reason: nil,
|
|
45
|
+
finish_reason_raw: nil
|
|
46
|
+
)
|
|
27
47
|
if finish_reason && !Riffer::Providers::FinishReason::VALUES.include?(finish_reason)
|
|
28
48
|
values = Riffer::Providers::FinishReason::VALUES.inspect
|
|
29
49
|
raise Riffer::ArgumentError, "finish_reason must be one of #{values}, got #{finish_reason.inspect}"
|
|
@@ -34,6 +54,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
34
54
|
@token_usage = token_usage
|
|
35
55
|
@structured_output = structured_output
|
|
36
56
|
@finish_reason = finish_reason
|
|
57
|
+
@finish_reason_raw = finish_reason_raw
|
|
37
58
|
end
|
|
38
59
|
|
|
39
60
|
#--
|
|
@@ -71,6 +92,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
71
92
|
hash[:token_usage] = token_usage.to_h if token_usage
|
|
72
93
|
hash[:structured_output] = structured_output if structured_output?
|
|
73
94
|
hash[:finish_reason] = finish_reason if finish_reason
|
|
95
|
+
hash[:finish_reason_raw] = finish_reason_raw if finish_reason_raw
|
|
74
96
|
hash
|
|
75
97
|
end
|
|
76
98
|
end
|
data/lib/riffer/messages/base.rb
CHANGED
|
@@ -14,38 +14,30 @@ class Riffer::Messages::Base
|
|
|
14
14
|
|
|
15
15
|
raise Riffer::ArgumentError, "Message must be a Hash or Message object, got #{msg.class}" unless msg.is_a?(Hash)
|
|
16
16
|
|
|
17
|
-
role
|
|
18
|
-
content = msg[:content]
|
|
17
|
+
raise Riffer::ArgumentError, "Message hash must include a 'role' key" if msg[:role].nil? || msg[:role].empty?
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
id = msg[:id]
|
|
23
|
-
|
|
24
|
-
case role.to_sym
|
|
19
|
+
case msg[:role].to_sym
|
|
25
20
|
when :user
|
|
26
21
|
files = (msg[:files] || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
|
|
27
|
-
Riffer::Messages::User.new(content, id: id, files: files)
|
|
22
|
+
Riffer::Messages::User.new(msg[:content], id: msg[:id], files: files)
|
|
28
23
|
when :assistant
|
|
29
24
|
tool_calls = (msg[:tool_calls] || []).map do |tc|
|
|
30
25
|
tc.is_a?(Riffer::Messages::Assistant::ToolCall) ? tc : Riffer::Messages::Assistant::ToolCall.new(**tc)
|
|
31
26
|
end
|
|
32
|
-
structured_output = msg[:structured_output]
|
|
33
|
-
finish_reason = msg[:finish_reason]&.to_sym
|
|
34
27
|
Riffer::Messages::Assistant.new(
|
|
35
|
-
content,
|
|
36
|
-
id: id,
|
|
28
|
+
msg[:content],
|
|
29
|
+
id: msg[:id],
|
|
37
30
|
tool_calls: tool_calls,
|
|
38
|
-
structured_output: structured_output,
|
|
39
|
-
finish_reason: finish_reason,
|
|
31
|
+
structured_output: msg[:structured_output],
|
|
32
|
+
finish_reason: msg[:finish_reason]&.to_sym,
|
|
33
|
+
finish_reason_raw: msg[:finish_reason_raw],
|
|
40
34
|
)
|
|
41
35
|
when :system
|
|
42
|
-
Riffer::Messages::System.new(content, id: id)
|
|
36
|
+
Riffer::Messages::System.new(msg[:content], id: msg[:id])
|
|
43
37
|
when :tool
|
|
44
|
-
|
|
45
|
-
name = msg[:name]
|
|
46
|
-
Riffer::Messages::Tool.new(content, id: id, tool_call_id: tool_call_id, name: name)
|
|
38
|
+
Riffer::Messages::Tool.new(msg[:content], id: msg[:id], tool_call_id: msg[:tool_call_id], name: msg[:name])
|
|
47
39
|
else
|
|
48
|
-
raise Riffer::ArgumentError, "Unknown message role: #{role}"
|
|
40
|
+
raise Riffer::ArgumentError, "Unknown message role: #{msg[:role]}"
|
|
49
41
|
end
|
|
50
42
|
end
|
|
51
43
|
|
|
@@ -96,7 +96,12 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
96
96
|
# Use strict schema to make optional fields nullable. Without this,
|
|
97
97
|
# Anthropic may return empty strings or whitespace instead of null
|
|
98
98
|
# for optional fields that the model has no value for.
|
|
99
|
+
#
|
|
100
|
+
# Merged over any caller-supplied output_config (e.g. effort) so those
|
|
101
|
+
# keys survive; the structured-output format wins because the run loop
|
|
102
|
+
# validates the response against it.
|
|
99
103
|
params[:output_config] = {
|
|
104
|
+
**(params[:output_config] || {}),
|
|
100
105
|
format: {
|
|
101
106
|
type: "json_schema",
|
|
102
107
|
schema: structured_output.json_schema(strict: true),
|
data/lib/riffer/runner/fibers.rb
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# +max_concurrency+ caps simultaneous fibers via an <tt>Async::Semaphore</tt>.
|
|
6
6
|
# If multiple fibers raise, only the first exception is re-raised after all
|
|
7
7
|
# finish.
|
|
8
|
+
# Joins the current reactor task when one is already running, and otherwise
|
|
9
|
+
# starts its own.
|
|
8
10
|
class Riffer::Runner::Fibers < Riffer::Runner
|
|
9
11
|
# @rbs @max_concurrency: Integer?
|
|
10
12
|
|
|
@@ -25,15 +27,15 @@ class Riffer::Runner::Fibers < Riffer::Runner
|
|
|
25
27
|
results = Array.new(items.size)
|
|
26
28
|
errors = Array.new(items.size)
|
|
27
29
|
|
|
28
|
-
Async
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
30
|
+
barrier = Async::Barrier.new
|
|
31
|
+
max = @max_concurrency
|
|
32
|
+
parent = if max
|
|
33
|
+
Async::Semaphore.new(max, parent: barrier)
|
|
34
|
+
else
|
|
35
|
+
barrier
|
|
36
|
+
end
|
|
36
37
|
|
|
38
|
+
Sync do
|
|
37
39
|
items.each_with_index do |item, index|
|
|
38
40
|
parent.async do
|
|
39
41
|
results[index] = yield(item)
|
|
@@ -43,6 +45,8 @@ class Riffer::Runner::Fibers < Riffer::Runner
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
barrier.wait
|
|
48
|
+
ensure
|
|
49
|
+
barrier.stop
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
first_error = errors.compact.first
|
data/lib/riffer/version.rb
CHANGED
data/sig/_private/async.rbs
CHANGED
|
@@ -10,6 +10,8 @@ module Async
|
|
|
10
10
|
def async: () { () -> void } -> untyped
|
|
11
11
|
|
|
12
12
|
def wait: () -> void
|
|
13
|
+
|
|
14
|
+
def stop: () -> void
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
class Semaphore
|
|
@@ -21,4 +23,6 @@ end
|
|
|
21
23
|
|
|
22
24
|
module Kernel
|
|
23
25
|
def Async: () { () -> void } -> untyped
|
|
26
|
+
|
|
27
|
+
def Sync: () { () -> void } -> untyped
|
|
24
28
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Generated from lib/riffer/agent/outcome.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# How a run ended — the single place to read whether the agent completed
|
|
4
|
+
# normally and, if not, why. +detail+ carries the specifics when there are any:
|
|
5
|
+
# the tripwire reason, the interrupt reason, the provider's raw finish value,
|
|
6
|
+
# or the structured output parse/validation error.
|
|
7
|
+
#
|
|
8
|
+
# response = agent.generate("Analyze this")
|
|
9
|
+
# case response.outcome.reason
|
|
10
|
+
# when :completed then puts response.structured_output
|
|
11
|
+
# when :invalid_structured_output then warn response.outcome.detail
|
|
12
|
+
# end
|
|
13
|
+
class Riffer::Agent::Outcome
|
|
14
|
+
# Finish reasons that end a turn normally; every other finish reason means the
|
|
15
|
+
# provider cut the turn short and surfaces as the run's outcome verbatim.
|
|
16
|
+
NORMAL_FINISH_REASONS: Array[Symbol]
|
|
17
|
+
|
|
18
|
+
# Derived from the provider vocabulary so a new finish reason becomes an
|
|
19
|
+
# outcome without a second list to update.
|
|
20
|
+
PROVIDER_STOP_REASONS: Array[Symbol]
|
|
21
|
+
|
|
22
|
+
# The vocabulary every run ends in.
|
|
23
|
+
VALUES: Array[Symbol]
|
|
24
|
+
|
|
25
|
+
# Why the run ended.
|
|
26
|
+
attr_reader reason: Symbol
|
|
27
|
+
|
|
28
|
+
# Human-readable specifics for +reason+, when there are any.
|
|
29
|
+
attr_reader detail: String?
|
|
30
|
+
|
|
31
|
+
# Raises Riffer::ArgumentError when +reason+ is outside VALUES.
|
|
32
|
+
# --
|
|
33
|
+
# : (reason: Symbol, ?detail: String?) -> void
|
|
34
|
+
def initialize: (reason: Symbol, ?detail: String?) -> void
|
|
35
|
+
|
|
36
|
+
# Returns true when the run completed normally.
|
|
37
|
+
#
|
|
38
|
+
# --
|
|
39
|
+
# : () -> bool
|
|
40
|
+
def success?: () -> bool
|
|
41
|
+
end
|
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
# Generated from lib/riffer/agent/response.rb with RBS::Inline
|
|
2
2
|
|
|
3
|
-
# Wraps an agent generation response.
|
|
4
|
-
# +content+ is empty and +tripwire+ carries the
|
|
3
|
+
# Wraps an agent generation response. +outcome+ says how the run ended; when a
|
|
4
|
+
# guardrail blocks execution, +content+ is empty and +tripwire+ carries the
|
|
5
|
+
# block details.
|
|
5
6
|
#
|
|
6
7
|
# response = agent.generate("Hello")
|
|
7
|
-
# if response.
|
|
8
|
-
# puts "Blocked: #{response.tripwire.reason}"
|
|
9
|
-
# else
|
|
8
|
+
# if response.outcome.success?
|
|
10
9
|
# puts response.content
|
|
10
|
+
# else
|
|
11
|
+
# puts "#{response.outcome.reason}: #{response.outcome.detail}"
|
|
11
12
|
# end
|
|
12
13
|
class Riffer::Agent::Response
|
|
13
|
-
@interrupted: bool
|
|
14
|
-
|
|
15
14
|
# The response content.
|
|
16
15
|
attr_reader content: String
|
|
17
16
|
|
|
17
|
+
# How the run ended.
|
|
18
|
+
attr_reader outcome: Riffer::Agent::Outcome
|
|
19
|
+
|
|
18
20
|
# The tripwire if execution was blocked.
|
|
19
21
|
attr_reader tripwire: Riffer::Guardrails::Tripwire?
|
|
20
22
|
|
|
21
23
|
# The modifications made by guardrails during processing.
|
|
22
24
|
attr_reader modifications: Array[Riffer::Guardrails::Modification]
|
|
23
25
|
|
|
24
|
-
# The reason provided with the interrupt, if any.
|
|
25
|
-
attr_reader interrupt_reason: (String | Symbol)?
|
|
26
|
-
|
|
27
26
|
# The parsed structured output, if structured output was configured.
|
|
28
27
|
attr_reader structured_output: Hash[Symbol, untyped]?
|
|
29
28
|
|
|
@@ -44,34 +43,20 @@ class Riffer::Agent::Response
|
|
|
44
43
|
# --
|
|
45
44
|
# : (
|
|
46
45
|
# String,
|
|
46
|
+
# outcome: Riffer::Agent::Outcome,
|
|
47
47
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
48
48
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
49
|
-
# ?interrupted: bool,
|
|
50
|
-
# ?interrupt_reason: (String | Symbol)?,
|
|
51
49
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
52
50
|
# ?messages: Array[Riffer::Messages::Base],
|
|
53
51
|
# ?healed_tool_call_ids: Array[String],
|
|
54
52
|
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
55
53
|
# ?steps: Integer
|
|
56
54
|
# ) -> void
|
|
57
|
-
def initialize: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?
|
|
58
|
-
|
|
59
|
-
# Returns true if the response was blocked by a guardrail.
|
|
60
|
-
#
|
|
61
|
-
# --
|
|
62
|
-
# : () -> bool
|
|
63
|
-
def blocked?: () -> bool
|
|
55
|
+
def initialize: (String, outcome: Riffer::Agent::Outcome, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?structured_output: Hash[Symbol, untyped]?, ?messages: Array[Riffer::Messages::Base], ?healed_tool_call_ids: Array[String], ?token_usage: Riffer::Providers::TokenUsage?, ?steps: Integer) -> void
|
|
64
56
|
|
|
65
57
|
# Returns true if any guardrail modified data during processing.
|
|
66
58
|
#
|
|
67
59
|
# --
|
|
68
60
|
# : () -> bool
|
|
69
61
|
def modified?: () -> bool
|
|
70
|
-
|
|
71
|
-
# Returns true if the agent loop was interrupted by a callback
|
|
72
|
-
# via <tt>throw :riffer_interrupt</tt>.
|
|
73
|
-
#
|
|
74
|
-
# --
|
|
75
|
-
# : () -> bool
|
|
76
|
-
def interrupted?: () -> bool
|
|
77
62
|
end
|
|
@@ -45,8 +45,16 @@ module Riffer::Agent::Run
|
|
|
45
45
|
def tripwire_response: (Riffer::Agent, Enumerator::Yielder?, Riffer::Guardrails::Tripwire, Array[Riffer::Guardrails::Modification], ?token_usage: Riffer::Providers::TokenUsage?, ?steps: Integer) -> Riffer::Agent::Response
|
|
46
46
|
|
|
47
47
|
# --
|
|
48
|
-
# : (Riffer::Agent, Array[Riffer::Guardrails::Modification], **untyped) -> Riffer::Agent::Response
|
|
49
|
-
def final_response: (Riffer::Agent, Array[Riffer::Guardrails::Modification], **untyped) -> Riffer::Agent::Response
|
|
48
|
+
# : (Riffer::Agent, Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, **untyped) -> Riffer::Agent::Response
|
|
49
|
+
def final_response: (Riffer::Agent, Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, **untyped) -> Riffer::Agent::Response
|
|
50
|
+
|
|
51
|
+
# Checked in the order things happened. The loop being stopped (max_steps or
|
|
52
|
+
# an interrupt) beats the provider's finish reason, which beats riffer's own
|
|
53
|
+
# validation of the content. A truncated response that also fails the schema
|
|
54
|
+
# therefore reports :length, not :invalid_structured_output.
|
|
55
|
+
# --
|
|
56
|
+
# : (Riffer::Messages::Assistant?, Riffer::Agent::StructuredOutput::Result?, interrupted: bool, interrupt_reason: (String | Symbol)?) -> Riffer::Agent::Outcome
|
|
57
|
+
def final_outcome: (Riffer::Messages::Assistant?, Riffer::Agent::StructuredOutput::Result?, interrupted: bool, interrupt_reason: (String | Symbol)?) -> Riffer::Agent::Outcome
|
|
50
58
|
|
|
51
59
|
# --
|
|
52
60
|
# : (Riffer::Agent, ?Hash[String, String]) -> Riffer::Messages::Assistant
|
|
@@ -77,8 +85,8 @@ module Riffer::Agent::Run
|
|
|
77
85
|
def run_after_guardrails: (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], ?Hash[String, String]) { (Riffer::Guardrails::Tripwire) -> void } -> untyped
|
|
78
86
|
|
|
79
87
|
# --
|
|
80
|
-
# : (Riffer::Agent, Riffer::Messages::Assistant?) ->
|
|
81
|
-
def
|
|
88
|
+
# : (Riffer::Agent, Riffer::Messages::Assistant?) -> Riffer::Agent::StructuredOutput::Result?
|
|
89
|
+
def structured_output_result: (Riffer::Agent, Riffer::Messages::Assistant?) -> Riffer::Agent::StructuredOutput::Result?
|
|
82
90
|
|
|
83
91
|
# --
|
|
84
92
|
# : (Riffer::Agent) -> Array[singleton(Riffer::Tool)]
|
|
@@ -96,16 +104,15 @@ module Riffer::Agent::Run
|
|
|
96
104
|
# : (
|
|
97
105
|
# Riffer::Agent,
|
|
98
106
|
# String,
|
|
107
|
+
# outcome: Riffer::Agent::Outcome,
|
|
99
108
|
# ?tripwire: Riffer::Guardrails::Tripwire?,
|
|
100
109
|
# ?modifications: Array[Riffer::Guardrails::Modification],
|
|
101
|
-
# ?interrupted: bool,
|
|
102
|
-
# ?interrupt_reason: (String | Symbol)?,
|
|
103
110
|
# ?structured_output: Hash[Symbol, untyped]?,
|
|
104
111
|
# ?healed_tool_call_ids: Array[String],
|
|
105
112
|
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
106
113
|
# ?steps: Integer
|
|
107
114
|
# ) -> Riffer::Agent::Response
|
|
108
|
-
def build_response: (Riffer::Agent, String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?
|
|
115
|
+
def build_response: (Riffer::Agent, String, outcome: Riffer::Agent::Outcome, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?structured_output: Hash[Symbol, untyped]?, ?healed_tool_call_ids: Array[String], ?token_usage: Riffer::Providers::TokenUsage?, ?steps: Integer) -> Riffer::Agent::Response
|
|
109
116
|
|
|
110
117
|
# Raises when +files+ are supplied without a +prompt+ — the provider needs
|
|
111
118
|
# text to anchor the attachments.
|
|
@@ -136,4 +143,8 @@ module Riffer::Agent::Run
|
|
|
136
143
|
# --
|
|
137
144
|
# : (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span, Riffer::Agent::Response) -> void
|
|
138
145
|
def record_run_outcome: (Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span, Riffer::Agent::Response) -> void
|
|
146
|
+
|
|
147
|
+
# --
|
|
148
|
+
# : (Riffer::Agent::Outcome) -> String?
|
|
149
|
+
def interrupt_reason_attribute: (Riffer::Agent::Outcome) -> String?
|
|
139
150
|
end
|
|
@@ -27,11 +27,23 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
27
27
|
# <tt>Riffer::Providers::FinishReason::VALUES</tt>).
|
|
28
28
|
attr_reader finish_reason: Symbol?
|
|
29
29
|
|
|
30
|
+
# The provider's raw finish-reason value behind +finish_reason+, when one
|
|
31
|
+
# exists on the wire.
|
|
32
|
+
attr_reader finish_reason_raw: String?
|
|
33
|
+
|
|
30
34
|
# Raises Riffer::ArgumentError when +finish_reason+ is outside the
|
|
31
35
|
# normalized vocabulary.
|
|
32
36
|
# --
|
|
33
|
-
# : (
|
|
34
|
-
|
|
37
|
+
# : (
|
|
38
|
+
# String,
|
|
39
|
+
# ?id: String?,
|
|
40
|
+
# ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall],
|
|
41
|
+
# ?token_usage: Riffer::Providers::TokenUsage?,
|
|
42
|
+
# ?structured_output: Hash[Symbol, untyped]?,
|
|
43
|
+
# ?finish_reason: Symbol?,
|
|
44
|
+
# ?finish_reason_raw: String?
|
|
45
|
+
# ) -> void
|
|
46
|
+
def initialize: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?, ?finish_reason: Symbol?, ?finish_reason_raw: String?) -> void
|
|
35
47
|
|
|
36
48
|
# --
|
|
37
49
|
# : () -> Symbol
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# +max_concurrency+ caps simultaneous fibers via an <tt>Async::Semaphore</tt>.
|
|
5
5
|
# If multiple fibers raise, only the first exception is re-raised after all
|
|
6
6
|
# finish.
|
|
7
|
+
# Joins the current reactor task when one is already running, and otherwise
|
|
8
|
+
# starts its own.
|
|
7
9
|
class Riffer::Runner::Fibers < Riffer::Runner
|
|
8
10
|
@max_concurrency: Integer?
|
|
9
11
|
|
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.46.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- lib/riffer/agent.rb
|
|
111
111
|
- lib/riffer/agent/config.rb
|
|
112
112
|
- lib/riffer/agent/context.rb
|
|
113
|
+
- lib/riffer/agent/outcome.rb
|
|
113
114
|
- lib/riffer/agent/response.rb
|
|
114
115
|
- lib/riffer/agent/run.rb
|
|
115
116
|
- lib/riffer/agent/serializer.rb
|
|
@@ -239,6 +240,7 @@ files:
|
|
|
239
240
|
- sig/generated/riffer/agent.rbs
|
|
240
241
|
- sig/generated/riffer/agent/config.rbs
|
|
241
242
|
- sig/generated/riffer/agent/context.rbs
|
|
243
|
+
- sig/generated/riffer/agent/outcome.rbs
|
|
242
244
|
- sig/generated/riffer/agent/response.rbs
|
|
243
245
|
- sig/generated/riffer/agent/run.rbs
|
|
244
246
|
- sig/generated/riffer/agent/serializer.rbs
|