riffer 0.47.1 → 0.48.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/.claude/rules/rbs-inline.md +5 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +31 -0
- data/Rakefile +1 -1
- data/Steepfile +1 -15
- data/docs/AGENTS.md +37 -3
- data/docs/AGENT_LIFECYCLE.md +1 -0
- data/docs/CONFIGURATION.md +1 -1
- data/docs/EVALS.md +2 -1
- data/docs/MCP.md +0 -4
- data/docs/MESSAGES.md +85 -17
- data/docs/STREAM_EVENTS.md +31 -4
- data/docs/TRACING.md +1 -1
- data/docs/providers/AMAZON_BEDROCK.md +1 -1
- data/docs/providers/CUSTOM_PROVIDERS.md +58 -4
- data/docs/providers/GEMINI.md +1 -1
- data/docs/providers/MOCK_PROVIDER.md +17 -0
- data/lib/riffer/agent/config.rb +40 -16
- data/lib/riffer/agent/outcome.rb +2 -2
- data/lib/riffer/agent/response.rb +16 -9
- data/lib/riffer/agent/run.rb +24 -9
- data/lib/riffer/agent/session.rb +2 -1
- data/lib/riffer/agent/structured_output/result.rb +2 -2
- data/lib/riffer/agent/structured_output.rb +1 -1
- data/lib/riffer/agent.rb +40 -16
- data/lib/riffer/config.rb +31 -31
- data/lib/riffer/evals/evaluator.rb +29 -2
- data/lib/riffer/evals/judge.rb +13 -8
- data/lib/riffer/evals/result.rb +6 -6
- data/lib/riffer/evals/run_result.rb +1 -1
- data/lib/riffer/evals/scenario_result.rb +6 -6
- data/lib/riffer/files/resolver.rb +3 -3
- data/lib/riffer/guardrails/modification.rb +3 -3
- data/lib/riffer/guardrails/result.rb +3 -3
- data/lib/riffer/guardrails/runner.rb +4 -4
- data/lib/riffer/guardrails/tripwire.rb +4 -4
- data/lib/riffer/helpers/deep_dup.rb +42 -0
- data/lib/riffer/mcp/manifest.rb +5 -5
- data/lib/riffer/mcp/registration.rb +1 -1
- data/lib/riffer/mcp/search_tool.rb +1 -1
- data/lib/riffer/messages/assistant/reasoning_part.rb +90 -0
- data/lib/riffer/messages/assistant/tool_call.rb +63 -0
- data/lib/riffer/messages/assistant.rb +55 -7
- data/lib/riffer/messages/base.rb +7 -25
- data/lib/riffer/messages/system.rb +10 -0
- data/lib/riffer/messages/tool.rb +21 -4
- data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +18 -17
- data/lib/riffer/messages/user.rb +13 -2
- data/lib/riffer/params/param.rb +20 -8
- data/lib/riffer/params.rb +10 -1
- data/lib/riffer/providers/amazon_bedrock.rb +34 -6
- data/lib/riffer/providers/anthropic.rb +9 -2
- data/lib/riffer/providers/base.rb +28 -10
- data/lib/riffer/providers/finish_reason.rb +2 -2
- data/lib/riffer/providers/gemini.rb +2 -2
- data/lib/riffer/providers/mock.rb +19 -3
- data/lib/riffer/providers/open_ai.rb +14 -5
- data/lib/riffer/providers/open_router.rb +6 -5
- data/lib/riffer/providers/repository.rb +0 -4
- data/lib/riffer/providers/token_usage.rb +21 -5
- data/lib/riffer/skills/adapter.rb +1 -1
- data/lib/riffer/skills/config.rb +12 -0
- data/lib/riffer/skills/context.rb +4 -7
- data/lib/riffer/skills/filesystem_backend.rb +1 -2
- data/lib/riffer/skills/frontmatter.rb +4 -4
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/base.rb +1 -1
- data/lib/riffer/stream_events/finish_reason_done.rb +2 -2
- data/lib/riffer/stream_events/guardrail_modification.rb +1 -1
- data/lib/riffer/stream_events/guardrail_tripwire.rb +1 -1
- data/lib/riffer/stream_events/interrupt.rb +2 -2
- data/lib/riffer/stream_events/reasoning_delta.rb +1 -1
- data/lib/riffer/stream_events/reasoning_done.rb +10 -8
- data/lib/riffer/stream_events/skill_activation.rb +1 -1
- data/lib/riffer/stream_events/text_delta.rb +1 -1
- data/lib/riffer/stream_events/text_done.rb +1 -1
- data/lib/riffer/stream_events/token_usage_done.rb +1 -1
- data/lib/riffer/stream_events/tool_call_delta.rb +3 -3
- data/lib/riffer/stream_events/tool_call_done.rb +4 -4
- data/lib/riffer/stream_events/web_search_done.rb +2 -2
- data/lib/riffer/stream_events/web_search_status.rb +3 -3
- data/lib/riffer/tools/response.rb +4 -4
- data/lib/riffer/tools/runtime.rb +3 -4
- data/lib/riffer/tracing/capture.rb +2 -4
- data/lib/riffer/tracing/stream_recorder.rb +6 -6
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +7 -0
- data/rbs_collection.lock.yaml +320 -0
- data/rbs_collection.yaml +12 -0
- data/sig/_private/anthropic.rbs +5 -4
- data/sig/_private/aws-sdk-core/event_error.rbs +15 -0
- data/sig/generated/riffer/agent/config.rbs +19 -5
- data/sig/generated/riffer/agent/response.rbs +6 -1
- data/sig/generated/riffer/agent/run.rbs +15 -8
- data/sig/generated/riffer/agent.rbs +25 -9
- data/sig/generated/riffer/config.rbs +1 -1
- data/sig/generated/riffer/evals/evaluator.rbs +20 -1
- data/sig/generated/riffer/evals/judge.rbs +5 -2
- data/sig/generated/riffer/files/resolver.rbs +6 -6
- data/sig/generated/riffer/helpers/deep_dup.rbs +21 -0
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +58 -0
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +42 -0
- data/sig/generated/riffer/messages/assistant.rbs +24 -10
- data/sig/generated/riffer/messages/system.rbs +6 -0
- data/sig/generated/riffer/messages/tool.rbs +6 -0
- data/sig/generated/riffer/messages/{file_part.rbs → user/file_part.rbs} +11 -6
- data/sig/generated/riffer/messages/user.rbs +9 -3
- data/sig/generated/riffer/params/param.rbs +7 -0
- data/sig/generated/riffer/params.rbs +6 -0
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +14 -5
- data/sig/generated/riffer/providers/anthropic.rbs +2 -2
- data/sig/generated/riffer/providers/base.rbs +22 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +7 -2
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +4 -4
- data/sig/generated/riffer/providers/repository.rbs +0 -2
- data/sig/generated/riffer/providers/token_usage.rbs +6 -0
- data/sig/generated/riffer/skills/config.rbs +9 -0
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +8 -6
- data/sig/generated/riffer/tools/runtime.rbs +2 -2
- data/sig/generated/riffer/tracing/capture.rbs +4 -4
- data/sig/generated/riffer.rbs +8 -0
- data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
- metadata +13 -5
- data/sig/_private/async.rbs +0 -28
- data/sig/_private/minitest.rbs +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5975a19de149eced0122ccf94f08666f66377c395b3a414db48158fcba352807
|
|
4
|
+
data.tar.gz: fe48ac55158f5bd1f4818651988b68ed2e45fa985c1ebe5a386d636decd15ff2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e506427c0b0272fb2c6f044def74b74d423e92bb23fbe95e02c5678d48b8b98ff2e8920807b23e4d8fb2db94d509937643813d9248eb109a51d03559a5ecd48
|
|
7
|
+
data.tar.gz: 3553da8e73fa137a4070baac5548fbd3a345316f0941e9f8b8490d2790d17a5e47b5eb154de6613897874a0a2bd3adaff4b1167d55fe21b99a1d0840d9786733
|
data/.claude/rules/rbs-inline.md
CHANGED
|
@@ -78,7 +78,11 @@ end
|
|
|
78
78
|
|
|
79
79
|
### Where stubs and stdlib deps live
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Dependency signatures come from [`rbs collection`](https://github.com/ruby/gem_rbs_collection): `rbs_collection.yaml` pins the upstream repo to a commit SHA, the gem set is derived from `Gemfile.lock`, and `rbs_collection.lock.yaml` is committed (the installed `.gem_rbs_collection/` is gitignored). Steep auto-detects the config, so the `Steepfile` declares no `library` lines. `yaml` is the one hand-listed entry under `gems:` — it's stdlib, so it never appears in `Gemfile.lock`.
|
|
82
|
+
|
|
83
|
+
`bin/typecheck` runs `rbs collection update` before `steep check`, keeping the lock in sync with `Gemfile.lock`; CI fails on lock drift. So after a Dependabot gem bump, run `bin/typecheck` and commit the refreshed lock. To pull newer upstream stubs, bump `revision` in `rbs_collection.yaml` and run `bin/typecheck` — pinning is what makes `update` deterministic.
|
|
84
|
+
|
|
85
|
+
- `sig/_private/` — signatures that must **not** ship. RBS **skips** `_`-prefixed directories in library mode, so consumers never load them; riffer's own `steep check` does (via the `Steepfile`). Two kinds, by predictable path: external-gem signatures are named by gem at the top level — full stubs for the gems upstream has nothing for (`mcp.rbs`, `zeitwerk.rbs`, `opentelemetry.rbs`, `rspec.rbs`, `aws-sdk-core/*`) plus arity patches for the provider SDKs (`openai.rbs`, `anthropic.rbs`); riffer's own hidden stubs mirror `lib/` under `riffer/` (e.g. `riffer/providers/anthropic.rbs` narrows the private `client` method to the SDK-typed client). A stub must be **deleted** once the collection covers that gem, or RBS raises `DuplicatedMethodDefinitionError`.
|
|
82
86
|
- `sig/manual/` — hand-written riffer-only signatures that are **safe to ship**, for the few things rbs-inline can't generate _at all_ (mirroring `lib/`). In practice that's `extend self` modules (`riffer/agent/run.rbs`, `riffer/helpers/call_or_value.rbs`) and modeling an include applied dynamically (`riffer/tools/toolable.rbs`). SDK-free ivars are **not** hand-written here — declare them inline with `# @rbs` (see "Annotation Conventions"). SDK-typed signatures can't ship, so they go in `_private/riffer/providers/` (the narrowed `client`).
|
|
83
87
|
- `sig/manifest.yaml` — declares the **stdlib** RBS the shipped sigs reference (`uri`, `net-http`) so `rbs -r riffer` resolves them.
|
|
84
88
|
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ 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.48.0](https://github.com/janeapp/riffer/compare/riffer/v0.47.2...riffer/v0.48.0) (2026-09-23)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* **messages:** Riffer::Messages::FilePart is renamed to Riffer::Messages::User::FilePart with no alias. ToolCall no longer responds to Struct-only methods such as [] or to_a; use its readers instead.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **agent:** inherit configuration on subclass ([#440](https://github.com/janeapp/riffer/issues/440)) ([a45c457](https://github.com/janeapp/riffer/commit/a45c45756108cfac7970e88b9f6f5b64638896c8))
|
|
18
|
+
* identify agents and evals with default tags ([#452](https://github.com/janeapp/riffer/issues/452)) ([22ab54c](https://github.com/janeapp/riffer/commit/22ab54c5225a45c38e255b1a2aa32be0ac28cb12))
|
|
19
|
+
* **messages:** persist reasoning on assistant messages ([#446](https://github.com/janeapp/riffer/issues/446)) ([7fc64ab](https://github.com/janeapp/riffer/commit/7fc64ab416feb47ccff575518378c58bbcc74d63))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **messages:** preserve tool errors and token usage through from_hash ([#450](https://github.com/janeapp/riffer/issues/450)) ([6e2bd90](https://github.com/janeapp/riffer/commit/6e2bd90926d7c3c071ac95f708875652268c767f))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Code Refactoring
|
|
28
|
+
|
|
29
|
+
* **messages:** make nested message objects follow one pattern ([#448](https://github.com/janeapp/riffer/issues/448)) ([e9e63db](https://github.com/janeapp/riffer/commit/e9e63dba16121692f734b476569b062c09cc959d))
|
|
30
|
+
|
|
31
|
+
## [0.47.2](https://github.com/janeapp/riffer/compare/riffer/v0.47.1...riffer/v0.47.2) (2026-09-21)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
* **providers:** raise IncompleteStreamError when a stream ends without its terminal event ([#445](https://github.com/janeapp/riffer/issues/445)) ([d142e23](https://github.com/janeapp/riffer/commit/d142e23e80d30ce73e81474eee35c71df1856c40))
|
|
37
|
+
* stop relying on the transitive cgi gem for CGI.escapeHTML ([#443](https://github.com/janeapp/riffer/issues/443)) ([821f3e3](https://github.com/janeapp/riffer/commit/821f3e3e405845b2cc275f86344e526cc2cf183a))
|
|
38
|
+
|
|
8
39
|
## [0.47.1](https://github.com/janeapp/riffer/compare/riffer/v0.47.0...riffer/v0.47.1) (2026-09-11)
|
|
9
40
|
|
|
10
41
|
|
data/Rakefile
CHANGED
data/Steepfile
CHANGED
|
@@ -9,19 +9,5 @@ target :lib do
|
|
|
9
9
|
|
|
10
10
|
check "lib"
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
library "aws-sdk-bedrockruntime"
|
|
14
|
-
library "aws-sdk-core"
|
|
15
|
-
library "base64"
|
|
16
|
-
library "cgi"
|
|
17
|
-
library "digest"
|
|
18
|
-
library "json"
|
|
19
|
-
library "logger"
|
|
20
|
-
library "net-http"
|
|
21
|
-
library "openai"
|
|
22
|
-
library "securerandom"
|
|
23
|
-
library "uri"
|
|
24
|
-
library "yaml"
|
|
25
|
-
|
|
26
|
-
configure_code_diagnostics(D::Ruby.strict)
|
|
12
|
+
configure_code_diagnostics(D::Ruby.all_error)
|
|
27
13
|
end
|
data/docs/AGENTS.md
CHANGED
|
@@ -126,7 +126,7 @@ end
|
|
|
126
126
|
|
|
127
127
|
### use_mcp
|
|
128
128
|
|
|
129
|
-
Loads tools from registered [MCP](MCP.md) servers by tag.
|
|
129
|
+
Loads tools from registered [MCP](MCP.md) servers by tag.
|
|
130
130
|
|
|
131
131
|
### model_options
|
|
132
132
|
|
|
@@ -265,7 +265,7 @@ class MyAgent < Riffer::Agent
|
|
|
265
265
|
end
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
-
Accepts a `Riffer::Tools::Runtime` subclass, a `Riffer::Tools::Runtime` instance, or a `Proc`. When unset,
|
|
268
|
+
Accepts a `Riffer::Tools::Runtime` subclass, a `Riffer::Tools::Runtime` instance, or a `Proc`. When unset, reads `Riffer.config.tool_runtime` at the point of use, so an agent that declares none follows a later change to the global. See [Tools — Tool Runtime](TOOL_ADVANCED.md#tool-runtime-experimental) for details.
|
|
269
269
|
|
|
270
270
|
### guardrail
|
|
271
271
|
|
|
@@ -304,6 +304,27 @@ MyAgent.config.max_steps # => 8
|
|
|
304
304
|
|
|
305
305
|
The DSL methods read and mutate this Config in place.
|
|
306
306
|
|
|
307
|
+
### Inheritance
|
|
308
|
+
|
|
309
|
+
A subclass starts from a copy of its parent's Config, so it inherits every setting the parent declared and overrides only what its own body declares:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
class BaseAgent < Riffer::Agent
|
|
313
|
+
model 'openai/gpt-5-mini'
|
|
314
|
+
max_steps 8
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
class TerseAgent < BaseAgent
|
|
318
|
+
max_steps 2
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
TerseAgent.config.model # => 'openai/gpt-5-mini'
|
|
322
|
+
TerseAgent.config.max_steps # => 2
|
|
323
|
+
BaseAgent.config.max_steps # => 8
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`identifier` is not inherited. A subclass derives its own from its class name, since two classes claiming one identifier would raise `Riffer::DuplicateIdentifierError` at the first lookup.
|
|
327
|
+
|
|
307
328
|
For advanced composition or testing, build a Config directly and pass it via `config:` to bypass class-level DSL entirely:
|
|
308
329
|
|
|
309
330
|
```ruby
|
|
@@ -372,13 +393,24 @@ agent.generate("Summarize this ticket.",
|
|
|
372
393
|
agent.stream("...", tags: {team: "growth", environment: "production"})
|
|
373
394
|
```
|
|
374
395
|
|
|
375
|
-
Keys and values may be `String` or `Symbol`; both are stringified, and entries with a `nil` value are dropped.
|
|
396
|
+
Keys and values may be `String` or `Symbol`; both are stringified, and entries with a `nil` value are dropped.
|
|
376
397
|
|
|
377
398
|
Tags propagate to **two** places:
|
|
378
399
|
|
|
379
400
|
1. The provider's native per-request metadata field (see the mapping below).
|
|
380
401
|
2. Observability — stamped as `riffer.tag.<key>` on **every** span the call emits (`invoke_agent`, `chat`, `execute_tool`, `execute_guardrail`). See [Tracing](TRACING.md).
|
|
381
402
|
|
|
403
|
+
### Default tags
|
|
404
|
+
|
|
405
|
+
Every call also carries two tags riffer adds itself, so a provider can tell who the call is on behalf of:
|
|
406
|
+
|
|
407
|
+
| Tag | Value |
|
|
408
|
+
| ------- | ----------------------------------------------------------------------- |
|
|
409
|
+
| `kind` | `"agent"` (`"judge"` for [evaluator](EVALS.md) judge calls) |
|
|
410
|
+
| `agent` | The agent's `identifier` (the evaluator's `identifier` for judge calls) |
|
|
411
|
+
|
|
412
|
+
A tag you pass with the same key wins. The default tags count towards the provider limits below.
|
|
413
|
+
|
|
382
414
|
### Reserved key: `user_id`
|
|
383
415
|
|
|
384
416
|
`user_id` is a reserved tag. Beyond appearing like any other tag, it maps to the provider's native end-user identifier where one exists (see the table).
|
|
@@ -393,6 +425,8 @@ Tags propagate to **two** places:
|
|
|
393
425
|
| Anthropic | `metadata.user_id` **only** | The only tag forwarded |
|
|
394
426
|
| Gemini | _(none — observability only)_ | Tag only; no request field |
|
|
395
427
|
|
|
428
|
+
If `model_options` already sets the native field (`metadata` or `request_metadata`), the tags are merged into it; a tag wins on a shared key.
|
|
429
|
+
|
|
396
430
|
**Anthropic silently drops non-`user_id` tags.** The Messages API has no free-form request-metadata field — only `metadata.user_id`. So for Anthropic, `user_id` is forwarded as `metadata: {user_id: …}` and **every other tag is dropped from the request** (it still appears on spans). This is intentional.
|
|
397
431
|
|
|
398
432
|
**Gemini is observability-only.** Riffer's Gemini adapter targets the Gemini Developer API (`generativelanguage.googleapis.com`), whose `generateContent` request has **no** `labels` field — sending unknown fields is rejected. So tags are **not** added to the Gemini request; they propagate to spans only. Native request labels (`labels`, lowercase `[a-z0-9_-]`, ≤63 chars each) are a Vertex AI feature and would arrive with a future Vertex adapter.
|
data/docs/AGENT_LIFECYCLE.md
CHANGED
|
@@ -315,6 +315,7 @@ agent.context[:skills] # the Skills::Context, if skills configured
|
|
|
315
315
|
| `content` | `String` | The response text |
|
|
316
316
|
| `outcome` | `Outcome` | How the run ended — `reason` and optional `detail` (see below) |
|
|
317
317
|
| `structured_output` | `Hash` / `nil` | Parsed and validated structured output (see below) |
|
|
318
|
+
| `reasoning` | `Array[ReasoningPart]` | The [reasoning parts](MESSAGES.md#reasoning) on the final assistant message (else `[]`) |
|
|
318
319
|
| `tripwire` | `Tripwire` / `nil` | The guardrail tripwire that blocked the request |
|
|
319
320
|
| `modified?` | `Boolean` | `true` if a guardrail modified the content |
|
|
320
321
|
| `modifications` | `Array` | List of guardrail modifications applied |
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -184,7 +184,7 @@ end
|
|
|
184
184
|
|
|
185
185
|
### File Downloads
|
|
186
186
|
|
|
187
|
-
File-attachment-download policy lives under `config.files`. Before an LLM call, riffer resolves every `Riffer::Messages::FilePart` attached to a user message against the provider's own capability — some providers accept a URL as-is, some need the bytes inline, and some can't take an attachment at all. See [Messages — File Parts](MESSAGES.md#file-parts) for `FilePart` itself and its `sha256:` field.
|
|
187
|
+
File-attachment-download policy lives under `config.files`. Before an LLM call, riffer resolves every `Riffer::Messages::User::FilePart` attached to a user message against the provider's own capability — some providers accept a URL as-is, some need the bytes inline, and some can't take an attachment at all. See [Messages — File Parts](MESSAGES.md#file-parts) for `FilePart` itself and its `sha256:` field.
|
|
188
188
|
|
|
189
189
|
```ruby
|
|
190
190
|
Riffer.configure do |config|
|
data/docs/EVALS.md
CHANGED
|
@@ -194,11 +194,12 @@ Class methods:
|
|
|
194
194
|
- `instructions(value)` - Evaluation criteria and scoring rubric (enables default `evaluate`)
|
|
195
195
|
- `higher_is_better(value)` - Whether higher scores are better (default: true)
|
|
196
196
|
- `judge_model(value)` - Override the global judge model
|
|
197
|
+
- `identifier(value)` - Override the identifier sent with judge calls (default: the snake_cased class name, or `riffer/judge` for an anonymous class)
|
|
197
198
|
|
|
198
199
|
Instance methods:
|
|
199
200
|
|
|
200
201
|
- `evaluate(input:, output:, ground_truth:, messages:)` - Override for custom logic; default calls judge with `instructions`
|
|
201
|
-
- `judge` - Returns a Judge instance for LLM-as-judge calls
|
|
202
|
+
- `judge` - Returns a Judge instance for LLM-as-judge calls. Its calls carry the [default tags](AGENTS.md#default-tags) `kind: "judge"` and `agent: <identifier>`
|
|
202
203
|
- `result(score:, reason:, metadata:, token_usage:)` - Helper to build Result objects
|
|
203
204
|
|
|
204
205
|
### Advanced: Custom Evaluate Override
|
data/docs/MCP.md
CHANGED
|
@@ -89,10 +89,6 @@ MCP tools are appended after any tools declared with `uses_tools`.
|
|
|
89
89
|
|
|
90
90
|
Tool names must be unique across `uses_tools` and all included MCP servers; duplicate names raise `Riffer::ArgumentError` when tools are resolved.
|
|
91
91
|
|
|
92
|
-
### Subclassing
|
|
93
|
-
|
|
94
|
-
Like [`uses_tools`](AGENTS.md#uses_tools), **`use_mcp` is not inherited** from the superclass. Declare `use_mcp` on each agent class that should load MCP tools.
|
|
95
|
-
|
|
96
92
|
## Progressive Tool Discovery
|
|
97
93
|
|
|
98
94
|
Progressive discovery is the default. The `use_mcp` instruction exposes **`mcp_search`** instead of flooding the context with every tool schema up front.
|
data/docs/MESSAGES.md
CHANGED
|
@@ -32,9 +32,9 @@ msg.to_h # => {role: :user, content: "Hello, how are you?"}
|
|
|
32
32
|
User messages can include file attachments:
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
|
-
file = Riffer::Messages::FilePart.from_path("photo.jpg")
|
|
35
|
+
file = Riffer::Messages::User::FilePart.from_path("photo.jpg")
|
|
36
36
|
msg = Riffer::Messages::User.new("Describe this image", files: [file])
|
|
37
|
-
msg.files # => [#<Riffer::Messages::FilePart ...>]
|
|
37
|
+
msg.files # => [#<Riffer::Messages::User::FilePart ...>]
|
|
38
38
|
msg.to_h # => {role: :user, content: "Describe this image", files: [{...}]}
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -67,6 +67,19 @@ if msg.token_usage
|
|
|
67
67
|
end
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
#### Tool Calls
|
|
71
|
+
|
|
72
|
+
`Riffer::Messages::Assistant::ToolCall` is the normalized container riffer stores a requested tool invocation in. Each call carries `call_id` (the provider's identifier, passed back as the tool result's `tool_call_id`), `name`, and `arguments` (the JSON-encoded argument string exactly as the provider emitted it); `to_h` serializes all three.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
tool_call = Riffer::Messages::Assistant::ToolCall.new(call_id: "call_123", name: "weather_tool", arguments: '{"city":"Tokyo"}')
|
|
76
|
+
msg = Riffer::Messages::Assistant.new("", tool_calls: [tool_call])
|
|
77
|
+
|
|
78
|
+
msg.has_tool_calls? # => true
|
|
79
|
+
msg.tool_calls.first.name # => "weather_tool"
|
|
80
|
+
tool_call.to_h # => {call_id: "call_123", name: "weather_tool", arguments: '{"city":"Tokyo"}'}
|
|
81
|
+
```
|
|
82
|
+
|
|
70
83
|
#### Token Usage Semantics
|
|
71
84
|
|
|
72
85
|
`TokenUsage` buckets carry the same meaning for every provider, regardless of how the provider reports its raw usage:
|
|
@@ -84,16 +97,16 @@ The cache buckets are subsets of `input_tokens`, never additions to it — summi
|
|
|
84
97
|
|
|
85
98
|
`finish_reason` carries the same meaning for every provider — each adapter maps its raw wire value (Anthropic's `end_turn`, OpenAI's response status, Gemini's `STOP`, …) into a normalized vocabulary:
|
|
86
99
|
|
|
87
|
-
| Value | Meaning
|
|
88
|
-
| ------------------- |
|
|
89
|
-
| `:stop` | The model finished its turn naturally (or hit a stop sequence).
|
|
90
|
-
| `:length` | Output was truncated at the max-token limit.
|
|
91
|
-
| `:tool_calls` | The model stopped to call tools.
|
|
92
|
-
| `:content_filter` | A provider safety system blocked or cut the response.
|
|
93
|
-
| `:context_window` | Input plus output hit the model's context window; trim or compact history rather than raising `max_tokens`.
|
|
100
|
+
| Value | Meaning |
|
|
101
|
+
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `:stop` | The model finished its turn naturally (or hit a stop sequence). |
|
|
103
|
+
| `:length` | Output was truncated at the max-token limit. |
|
|
104
|
+
| `:tool_calls` | The model stopped to call tools. |
|
|
105
|
+
| `:content_filter` | A provider safety system blocked or cut the response. |
|
|
106
|
+
| `:context_window` | Input plus output hit the model's context window; trim or compact history rather than raising `max_tokens`. |
|
|
94
107
|
| `:malformed_output` | The model emitted output the provider could not parse, such as an invalid tool call; retry or nudge rather than backing off. |
|
|
95
|
-
| `:error` | The provider reported an error finish.
|
|
96
|
-
| `:other` | A provider-specific value with no normalized equivalent.
|
|
108
|
+
| `:error` | The provider reported an error finish. |
|
|
109
|
+
| `:other` | A provider-specific value with no normalized equivalent. |
|
|
97
110
|
|
|
98
111
|
`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:
|
|
99
112
|
|
|
@@ -124,6 +137,61 @@ msg = Riffer::Messages::Assistant.new('{"sentiment":"positive"}', structured_out
|
|
|
124
137
|
msg.to_h # => {role: :assistant, content: '{"sentiment":"positive"}', structured_output: {sentiment: "positive"}}
|
|
125
138
|
```
|
|
126
139
|
|
|
140
|
+
#### Reasoning
|
|
141
|
+
|
|
142
|
+
Reasoning models emit thinking blocks alongside their answer, and several providers require those blocks back on the next turn of a tool-calling loop. `Riffer::Messages::Assistant::ReasoningPart` is the normalized container riffer stores them in: a list of parts on the assistant message, in the order the provider emitted them.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
summary = Riffer::Messages::Assistant::ReasoningPart.new(type: :summary, text: "The user wants the answer.", format: "mock-v1")
|
|
146
|
+
opaque = Riffer::Messages::Assistant::ReasoningPart.new(type: :encrypted, data: "b3BhcXVl", format: "mock-v1")
|
|
147
|
+
msg = Riffer::Messages::Assistant.new("42", reasoning: [summary, opaque])
|
|
148
|
+
|
|
149
|
+
msg.reasoning? # => true
|
|
150
|
+
msg.reasoning_text # => "The user wants the answer."
|
|
151
|
+
msg.reasoning.first.type # => :summary
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A readable part next to an opaque one is the common shape, not a contrived one: OpenAI's Responses API returns a reasoning item as summary text plus an encrypted payload, and Anthropic pairs a `thinking` block with a `redacted_thinking` block when it redacts part of the chain of thought.
|
|
155
|
+
|
|
156
|
+
`reasoning:` takes `ReasoningPart`s; `Riffer::Messages::Base.from_hash` is what turns persisted hashes back into parts.
|
|
157
|
+
|
|
158
|
+
Each part carries:
|
|
159
|
+
|
|
160
|
+
| Field | Type | Description |
|
|
161
|
+
| ----------- | --------- | --------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `type` | `Symbol` | One of `:text` (readable reasoning), `:summary` (a provider-condensed digest), `:encrypted` (an opaque payload) |
|
|
163
|
+
| `text` | `String?` | The reasoning prose, for `:text` and `:summary` parts |
|
|
164
|
+
| `data` | `String?` | The opaque payload, for `:encrypted` parts |
|
|
165
|
+
| `signature` | `String?` | The provider's signature over the part, when it issues one |
|
|
166
|
+
| `id` | `String?` | The provider's identifier for the part, when it issues one |
|
|
167
|
+
| `format` | `String?` | The wire format, owned by the adapter that produced the part (e.g. `"anthropic-claude-v1"`) |
|
|
168
|
+
|
|
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
|
+
|
|
171
|
+
`reasoning?` is true when the message carries any part. `reasoning_text` joins the `text` of the `:text` and `:summary` parts with blank lines, skipping `:encrypted` parts, and is `nil` when there is nothing to join. The run's final assistant message projects its parts onto `response.reasoning` (see [Agent Lifecycle — Response Attributes](AGENT_LIFECYCLE.md#response-attributes)).
|
|
172
|
+
|
|
173
|
+
Parts round-trip through `to_h` / `from_hash` like every other message field, so an application that persists sessions can store and replay them. The `reasoning` key is absent from `to_h` when the message has no parts, and each part omits the fields it doesn't carry:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
msg.to_h
|
|
177
|
+
# => {role: :assistant, content: "42", reasoning: [
|
|
178
|
+
# {type: :summary, text: "The user wants the answer.", format: "mock-v1"},
|
|
179
|
+
# {type: :encrypted, data: "b3BhcXVl", format: "mock-v1"}
|
|
180
|
+
# ]}
|
|
181
|
+
|
|
182
|
+
Riffer::Messages::Base.from_hash(msg.to_h).reasoning # => [ReasoningPart, ReasoningPart]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**The replay contract.** A provider adapter replays only the parts whose `format` it recognizes and silently skips the rest, so history that travelled through another provider is never rejected. A part with no `format` is never replayed; adapters that surface reasoning text but cannot yet send it back emit their parts that way, so the text is kept for display without risking a rejected request. Parts are never reordered, merged, or edited — riffer treats them as opaque, because the provider's signature covers their exact bytes.
|
|
186
|
+
|
|
187
|
+
An application that would rather not store parts can leave the key out when it serializes:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
msg.to_h.except(:reasoning)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
To drop parts from the in-memory session mid-run instead, `Session#update(id:, reasoning: [])` rewrites the message in place; it needs [message ids](#ids) enabled to address it.
|
|
194
|
+
|
|
127
195
|
### Tool
|
|
128
196
|
|
|
129
197
|
Tool messages contain the results of tool executions:
|
|
@@ -155,7 +223,7 @@ msg.error_type # => :execution_error
|
|
|
155
223
|
|
|
156
224
|
## File Parts
|
|
157
225
|
|
|
158
|
-
`Riffer::Messages::FilePart` represents a file attachment (image or document) that can be included with user messages.
|
|
226
|
+
`Riffer::Messages::User::FilePart` represents a file attachment (image or document) that can be included with user messages.
|
|
159
227
|
|
|
160
228
|
### Supported Media Types
|
|
161
229
|
|
|
@@ -167,21 +235,21 @@ msg.error_type # => :execution_error
|
|
|
167
235
|
|
|
168
236
|
```ruby
|
|
169
237
|
# From a file path (reads eagerly, detects media type from extension)
|
|
170
|
-
file = Riffer::Messages::FilePart.from_path("photo.jpg")
|
|
238
|
+
file = Riffer::Messages::User::FilePart.from_path("photo.jpg")
|
|
171
239
|
file.media_type # => "image/jpeg"
|
|
172
240
|
file.filename # => "photo.jpg"
|
|
173
241
|
file.image? # => true
|
|
174
242
|
|
|
175
243
|
# From a URL (stored directly, resolved lazily if provider needs bytes)
|
|
176
|
-
file = Riffer::Messages::FilePart.from_url("https://example.com/doc.pdf")
|
|
244
|
+
file = Riffer::Messages::User::FilePart.from_url("https://example.com/doc.pdf")
|
|
177
245
|
file.url? # => true
|
|
178
246
|
file.document? # => true
|
|
179
247
|
|
|
180
248
|
# From raw base64 data
|
|
181
|
-
file = Riffer::Messages::FilePart.new(media_type: "image/png", data: base64_string, filename: "chart.png")
|
|
249
|
+
file = Riffer::Messages::User::FilePart.new(media_type: "image/png", data: base64_string, filename: "chart.png")
|
|
182
250
|
|
|
183
251
|
# With an expected sha256 checksum of the file's contents
|
|
184
|
-
file = Riffer::Messages::FilePart.from_url(
|
|
252
|
+
file = Riffer::Messages::User::FilePart.from_url(
|
|
185
253
|
"https://example.com/doc.pdf",
|
|
186
254
|
sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
187
255
|
)
|
|
@@ -243,7 +311,7 @@ agent = MyAgent.new(session: session)
|
|
|
243
311
|
response = agent.generate # session already carries the last user turn
|
|
244
312
|
```
|
|
245
313
|
|
|
246
|
-
`Riffer::Agent::Session.new(messages:)` accepts `Riffer::Messages::Base` objects. If your persistence layer hands back hashes, normalize them first via `Riffer::Messages::Base.from_hash` or your own adapter.
|
|
314
|
+
`Riffer::Agent::Session.new(messages:)` accepts `Riffer::Messages::Base` objects. If your persistence layer hands back hashes, normalize them first via `Riffer::Messages::Base.from_hash` (which dispatches on `:role`), a role's own `from_hash` such as `Riffer::Messages::User.from_hash` when the role is already known, or your own adapter.
|
|
247
315
|
|
|
248
316
|
### Accessing Message History
|
|
249
317
|
|
data/docs/STREAM_EVENTS.md
CHANGED
|
@@ -105,14 +105,18 @@ event.content # => "Let me think about "
|
|
|
105
105
|
|
|
106
106
|
### ReasoningDone
|
|
107
107
|
|
|
108
|
-
Emitted when reasoning is complete:
|
|
108
|
+
Emitted when one reasoning block is complete:
|
|
109
109
|
|
|
110
110
|
```ruby
|
|
111
|
-
|
|
112
|
-
event
|
|
113
|
-
event.
|
|
111
|
+
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: "Let me think about this step by step...", format: "mock-v1")
|
|
112
|
+
event = Riffer::StreamEvents::ReasoningDone.new(part)
|
|
113
|
+
event.role # => :assistant
|
|
114
|
+
event.part # => the ReasoningPart
|
|
115
|
+
event.part.text # => "Let me think about this step by step..."
|
|
114
116
|
```
|
|
115
117
|
|
|
118
|
+
`part` is the [reasoning part](MESSAGES.md#reasoning) the preceding `ReasoningDelta` events added up to, and the agent loop accumulates it onto the assistant message. Adapters that cannot yet replay their reasoning emit it as a `:text` part with no `format`, so it is stored for display but never sent back to the provider.
|
|
119
|
+
|
|
116
120
|
### WebSearchStatus
|
|
117
121
|
|
|
118
122
|
Emitted during web search progress with status updates:
|
|
@@ -272,6 +276,29 @@ event.to_h # => {role: :assistant, finish_reason: :length, raw_fin
|
|
|
272
276
|
|
|
273
277
|
The agent loop stamps this value onto the accumulated assistant message's `finish_reason`.
|
|
274
278
|
|
|
279
|
+
## Incomplete Streams
|
|
280
|
+
|
|
281
|
+
If a provider's stream ends before its terminal event, the enumerator raises `Riffer::IncompleteStreamError` (a `Riffer::Error` subclass) instead of finishing normally, so a truncated or empty response is never returned as a complete message. Events already yielded before the raise were delivered as usual, but nothing from the failed step is added to the session: there is no partial assistant message to resume from, and messages from earlier completed steps (tool calls and their results) are untouched.
|
|
282
|
+
|
|
283
|
+
Supported on Amazon Bedrock, Anthropic, and OpenAI / Azure OpenAI.
|
|
284
|
+
|
|
285
|
+
The user prompt is added to the session before the run starts and stays there after the failure, so retry with `agent.stream` and no prompt. Passing the prompt again would add a second user turn.
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
attempts = 0
|
|
289
|
+
prompt = "Tell me a story"
|
|
290
|
+
begin
|
|
291
|
+
agent.stream(prompt).each do |event|
|
|
292
|
+
print event.content if event.is_a?(Riffer::StreamEvents::TextDelta)
|
|
293
|
+
end
|
|
294
|
+
rescue Riffer::IncompleteStreamError => e
|
|
295
|
+
warn "stream ended early: #{e.message}"
|
|
296
|
+
prompt = nil # already in the session; re-run on the existing history
|
|
297
|
+
retry if (attempts += 1) < 3
|
|
298
|
+
raise
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
275
302
|
## Streaming with Tools
|
|
276
303
|
|
|
277
304
|
When an agent uses tools during streaming, the flow is:
|
data/docs/TRACING.md
CHANGED
|
@@ -95,7 +95,7 @@ The contract promise is: **when present**, a key carries the documented meaning
|
|
|
95
95
|
|
|
96
96
|
### Per-call tags (`riffer.tag.*`)
|
|
97
97
|
|
|
98
|
-
Any tags passed to `#generate` / `#stream` via `tags:` are stamped on **all four** span types as `riffer.tag.<key>` (string), so the per-span tables below omit them. They appear on every span the tagged call emits and are absent otherwise. Example: `tags: {team: "growth"}` adds `riffer.tag.team` → `"growth"` to the `invoke_agent`, `chat`, `execute_tool`, and `execute_guardrail` spans. See [Per-Call Tags](AGENTS.md#per-call-tags) for the full surface and the per-provider request-metadata mapping.
|
|
98
|
+
Any tags passed to `#generate` / `#stream` via `tags:` are stamped on **all four** span types as `riffer.tag.<key>` (string), so the per-span tables below omit them. They appear on every span the tagged call emits and are absent otherwise. Example: `tags: {team: "growth"}` adds `riffer.tag.team` → `"growth"` to the `invoke_agent`, `chat`, `execute_tool`, and `execute_guardrail` spans. The [default tags](AGENTS.md#default-tags) are always present, e.g. `riffer.tag.kind` → `"agent"` and `riffer.tag.agent` → the agent identifier. See [Per-Call Tags](AGENTS.md#per-call-tags) for the full surface and the per-provider request-metadata mapping.
|
|
99
99
|
|
|
100
100
|
## `invoke_agent {agent}` — the run span
|
|
101
101
|
|
|
@@ -171,7 +171,7 @@ end
|
|
|
171
171
|
Bedrock accepts file attachments either as raw bytes, or as `s3://` URIs passed straight through to Converse — Bedrock fetches the S3 object itself:
|
|
172
172
|
|
|
173
173
|
```ruby
|
|
174
|
-
file = Riffer::Messages::FilePart.from_url("s3://my-bucket/document.pdf", media_type: "application/pdf")
|
|
174
|
+
file = Riffer::Messages::User::FilePart.from_url("s3://my-bucket/document.pdf", media_type: "application/pdf")
|
|
175
175
|
response = provider.generate_text(
|
|
176
176
|
prompt: "Summarize this document",
|
|
177
177
|
model: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
@@ -24,7 +24,7 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
|
24
24
|
params = {
|
|
25
25
|
model: model,
|
|
26
26
|
messages: convert_messages(messages),
|
|
27
|
-
**options.except(:tools)
|
|
27
|
+
**options.except(:tools, :tags)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if tools && !tools.empty?
|
|
@@ -220,9 +220,9 @@ Riffer::StreamEvents::ToolCallDone.new(
|
|
|
220
220
|
arguments: '{"complete":"args"}'
|
|
221
221
|
)
|
|
222
222
|
|
|
223
|
-
# Reasoning (if supported)
|
|
223
|
+
# Reasoning (if supported); see the Reasoning section for building the part
|
|
224
224
|
Riffer::StreamEvents::ReasoningDelta.new("thinking...")
|
|
225
|
-
Riffer::StreamEvents::ReasoningDone.new(
|
|
225
|
+
Riffer::StreamEvents::ReasoningDone.new(part)
|
|
226
226
|
|
|
227
227
|
# Web search (if supported)
|
|
228
228
|
Riffer::StreamEvents::WebSearchStatus.new("searching", query: "search query")
|
|
@@ -274,6 +274,60 @@ For streaming, emit a `FinishReasonDone` event near the end of `execute_stream`:
|
|
|
274
274
|
yielder << Riffer::StreamEvents::FinishReasonDone.new(finish_reason: :stop, raw_finish_reason: "done")
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
+
Also have `execute_stream` raise `Riffer::IncompleteStreamError` when the stream ends without the provider's terminal event, rather than returning normally. Otherwise a connection that drops mid-response looks identical to a finished one, and the agent loop accepts a truncated message as complete.
|
|
278
|
+
|
|
279
|
+
## Reasoning
|
|
280
|
+
|
|
281
|
+
`extract_reasoning` is the optional hook for reasoning models — return the response's thinking blocks as [`Riffer::Messages::Assistant::ReasoningPart`s](../MESSAGES.md#reasoning) and the base class attaches them to the assistant message, where your application can persist them and hand them back on the next turn:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
def extract_reasoning(response)
|
|
285
|
+
response.thinking_blocks.map do |block|
|
|
286
|
+
Riffer::Messages::Assistant::ReasoningPart.new(
|
|
287
|
+
type: :encrypted,
|
|
288
|
+
data: block.data,
|
|
289
|
+
signature: block.signature,
|
|
290
|
+
format: "my-provider-v1"
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The base class defaults to `[]`, so a provider without reasoning stays valid.
|
|
297
|
+
|
|
298
|
+
Your adapter owns its `format` string: pick one value per wire shape, replay only the parts carrying a value you recognize, and skip the rest — history that travelled through another provider must never make a request fail. Never reorder or edit a part; the provider's signature covers its exact bytes.
|
|
299
|
+
|
|
300
|
+
For streaming, emit one `ReasoningDone` per block, carrying the part so the agent loop can accumulate it:
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: "complete reasoning", format: "my-provider-v1")
|
|
304
|
+
|
|
305
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new("thinking...")
|
|
306
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
If your adapter surfaces reasoning text but cannot yet replay it, call `yield_reasoning_done(yielder, text)` instead. It wraps the text in a `:text` part with no `format`, which persists for display and is skipped on replay.
|
|
310
|
+
|
|
311
|
+
## Tags
|
|
312
|
+
|
|
313
|
+
Every agent and judge call passes a `:tags` option: a flat `String => String` hash of the caller's [per-call tags](../AGENTS.md#per-call-tags) plus the [default tags](../AGENTS.md#default-tags). `kind` (`"agent"` or `"judge"`) and `agent` (the agent or evaluator identifier) tell you who the call is on behalf of:
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
def build_request_params(messages, model, options)
|
|
317
|
+
tags = options[:tags] || {}
|
|
318
|
+
|
|
319
|
+
{
|
|
320
|
+
agent: tags["agent"],
|
|
321
|
+
user: tags["user_id"],
|
|
322
|
+
messages: convert_messages(messages),
|
|
323
|
+
model: model,
|
|
324
|
+
**options.except(:tools, :tags),
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Map tags to your service's native request field, or drop them, but don't pass `:tags` on to an SDK verbatim.
|
|
330
|
+
|
|
277
331
|
## Trace Provider Name
|
|
278
332
|
|
|
279
333
|
LLM-call and agent-run spans stamp `gen_ai.provider.name` from the `semconv_provider_name` class method. The default is your snake_cased class name; override it when a [GenAI semconv well-known value](https://opentelemetry.io/docs/specs/semconv/gen-ai/) exists for your provider:
|
|
@@ -328,7 +382,7 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
|
328
382
|
messages: convert_messages(conversation),
|
|
329
383
|
system: system_message,
|
|
330
384
|
max_tokens: options[:max_tokens] || 4096,
|
|
331
|
-
**options.except(:tools, :max_tokens)
|
|
385
|
+
**options.except(:tools, :max_tokens, :tags)
|
|
332
386
|
}
|
|
333
387
|
|
|
334
388
|
if tools && !tools.empty?
|
data/docs/providers/GEMINI.md
CHANGED
|
@@ -148,7 +148,7 @@ response = provider.generate_text(
|
|
|
148
148
|
Gemini's API only accepts inline base64-encoded files (images and documents), never a URL reference:
|
|
149
149
|
|
|
150
150
|
```ruby
|
|
151
|
-
file = Riffer::Messages::FilePart.new(data: base64_data, media_type: "image/png")
|
|
151
|
+
file = Riffer::Messages::User::FilePart.new(data: base64_data, media_type: "image/png")
|
|
152
152
|
response = provider.generate_text(
|
|
153
153
|
prompt: "Describe this image",
|
|
154
154
|
model: "gemini-2.5-flash-lite",
|
|
@@ -47,6 +47,23 @@ provider.stub_response("Based on the tool result, here's my answer.")
|
|
|
47
47
|
response = agent.generate("Use the tool")
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Stubbing Reasoning
|
|
51
|
+
|
|
52
|
+
Stub [reasoning parts](../MESSAGES.md#reasoning) to exercise your application's persistence of them. Hashes are normalized into `Riffer::Messages::Assistant::ReasoningPart`s:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
provider.stub_response("42", reasoning: [
|
|
56
|
+
{type: :text, text: "The user wants the answer.", format: "mock-v1"},
|
|
57
|
+
{type: :encrypted, data: "b3BhcXVl", signature: "sig", format: "mock-v1"}
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
response = agent.generate("What is the answer?")
|
|
61
|
+
response.reasoning.map(&:type) # => [:text, :encrypted]
|
|
62
|
+
agent.session.messages.last.reasoning_text # => "The user wants the answer."
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When streaming, each part is emitted as a `ReasoningDelta` (only when it carries `text`) followed by a `ReasoningDone` carrying the part, ahead of the text events.
|
|
66
|
+
|
|
50
67
|
## Queueing Multiple Responses
|
|
51
68
|
|
|
52
69
|
Responses are consumed in order:
|