riffer 0.40.0 → 0.42.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/{AGENTS.md → .claude/CLAUDE.md} +0 -8
  3. data/.claude/rules/comments.md +13 -0
  4. data/.claude/rules/rbs-inline.md +93 -0
  5. data/.release-please-manifest.json +1 -1
  6. data/CHANGELOG.md +22 -0
  7. data/README.md +20 -24
  8. data/docs/AGENTS.md +24 -14
  9. data/docs/AGENT_LIFECYCLE.md +2 -2
  10. data/docs/CONFIGURATION.md +105 -21
  11. data/docs/SERIALIZATION.md +4 -3
  12. data/docs/TOOLS.md +18 -1
  13. data/docs/providers/AMAZON_BEDROCK.md +14 -8
  14. data/docs/providers/ANTHROPIC.md +9 -7
  15. data/docs/providers/AZURE_OPENAI.md +12 -12
  16. data/docs/providers/CUSTOM_PROVIDERS.md +32 -16
  17. data/docs/providers/GEMINI.md +28 -5
  18. data/docs/providers/OPENAI.md +21 -5
  19. data/docs/providers/OPENROUTER.md +11 -6
  20. data/docs/providers/PROVIDERS.md +16 -1
  21. data/lib/riffer/agent/config.rb +0 -6
  22. data/lib/riffer/agent/run.rb +2 -4
  23. data/lib/riffer/agent/serializer.rb +0 -2
  24. data/lib/riffer/agent.rb +4 -26
  25. data/lib/riffer/config.rb +6 -6
  26. data/lib/riffer/evals/judge.rb +3 -5
  27. data/lib/riffer/guardrail.rb +1 -1
  28. data/lib/riffer/helpers/identifier.rb +41 -0
  29. data/lib/riffer/providers/amazon_bedrock.rb +31 -19
  30. data/lib/riffer/providers/anthropic.rb +23 -8
  31. data/lib/riffer/providers/azure_open_ai.rb +19 -11
  32. data/lib/riffer/providers/base.rb +32 -1
  33. data/lib/riffer/providers/gemini/client.rb +120 -0
  34. data/lib/riffer/providers/gemini.rb +13 -62
  35. data/lib/riffer/providers/mock.rb +5 -4
  36. data/lib/riffer/providers/open_ai.rb +23 -8
  37. data/lib/riffer/providers/open_router.rb +23 -9
  38. data/lib/riffer/registrable.rb +81 -0
  39. data/lib/riffer/tool.rb +1 -0
  40. data/lib/riffer/tools/toolable.rb +2 -3
  41. data/lib/riffer/version.rb +1 -1
  42. data/lib/riffer.rb +3 -0
  43. data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -2
  44. data/sig/_private/riffer/providers/anthropic.rbs +4 -2
  45. data/sig/_private/riffer/providers/gemini.rbs +7 -0
  46. data/sig/_private/riffer/providers/open_ai.rbs +4 -2
  47. data/sig/_private/riffer/providers/open_router.rbs +4 -2
  48. data/sig/generated/riffer/agent/config.rbs +1 -5
  49. data/sig/generated/riffer/agent.rbs +2 -18
  50. data/sig/generated/riffer/config.rbs +25 -15
  51. data/sig/generated/riffer/evals/judge.rbs +2 -4
  52. data/sig/generated/riffer/helpers/identifier.rbs +19 -0
  53. data/sig/generated/riffer/providers/amazon_bedrock.rbs +13 -2
  54. data/sig/generated/riffer/providers/anthropic.rbs +13 -2
  55. data/sig/generated/riffer/providers/azure_open_ai.rbs +14 -4
  56. data/sig/generated/riffer/providers/base.rbs +22 -0
  57. data/sig/generated/riffer/providers/gemini/client.rbs +65 -0
  58. data/sig/generated/riffer/providers/gemini.rbs +7 -23
  59. data/sig/generated/riffer/providers/mock.rbs +4 -3
  60. data/sig/generated/riffer/providers/open_ai.rbs +13 -2
  61. data/sig/generated/riffer/providers/open_router.rbs +16 -3
  62. data/sig/generated/riffer/registrable.rbs +51 -0
  63. data/sig/generated/riffer/tool.rbs +2 -0
  64. data/sig/generated/riffer/tools/toolable.rbs +3 -1
  65. data/sig/generated/riffer.rbs +4 -0
  66. data/sig/manual/riffer/agent.rbs +7 -0
  67. data/sig/manual/riffer/helpers/identifier.rbs +5 -0
  68. data/sig/manual/riffer/tool.rbs +7 -0
  69. metadata +14 -11
  70. data/.agents/architecture.md +0 -265
  71. data/.agents/code-style.md +0 -110
  72. data/.agents/providers.md +0 -45
  73. data/.agents/rbs-inline.md +0 -169
  74. data/.agents/testing.md +0 -60
  75. data/CLAUDE.md +0 -1
  76. data/lib/riffer/helpers/class_name_converter.rb +0 -22
  77. data/sig/generated/riffer/helpers/class_name_converter.rbs +0 -12
  78. data/sig/manual/riffer/helpers/class_name_converter.rbs +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c89776aeb032fbf837df103366fed830edcc20769cc9d4958a3a9bfa56a7803
4
- data.tar.gz: 2153cf2967b9b70366987aeb9e59b215ff641bcd44668525c07d822ff9757579
3
+ metadata.gz: eabed1a1a6844787d92f31be0a72e210117005c19b4dd5e021a905121f83e8e1
4
+ data.tar.gz: 4cc6b6bde7e4b474d7ca6fb2672a3e115d4584b096f1de5d967a2b1a751f49ba
5
5
  SHA512:
6
- metadata.gz: edfb2a10f17e1c0f6fb0630f5c2244370e01c5f32d05e9f065183da0b5b2d2cfec2453cac9a08714bd921716cd747609e4db61fb105a57769d79ab2f8988356a
7
- data.tar.gz: 7d6dcdbab4be152033fcf75581252d3c9bda60c9e49825e75028d19670bae0490087ee8a90267a2af2f4fd8daabb5f370c8c2b7bb7cdcffe77a75b94a6b190ff
6
+ metadata.gz: 220febe23864053fa767e3eecc6850dc0c25ebdcb363a86d5daa58cd317c5500d08f2b50f499f133a77d7b4cf5182485c6faf0ddf36550099b22b7dd203af0c6
7
+ data.tar.gz: d231a4b0dc8c0468f4cba1abf9e3ace390852efc4b6adf5c23229c6729d4ad71fb0afdd5ae5b50540a188b79ec519ef58f95972357fb9854398570d0de058781
@@ -10,14 +10,6 @@ Ruby gem framework for building AI-powered agents with LLM provider adapters.
10
10
  - **Model format**: `provider/model` (e.g., `openai/gpt-4`)
11
11
  - **Docs**: when adding a public config option or message attribute, update the matching page in `docs/` (e.g., `docs/CONFIGURATION.md`, `docs/MESSAGES.md`). Adding a NEW page under `docs/` also requires a `docs-site/manifest.yml` entry — the site build fails without one. RDoc ≠ user docs.
12
12
 
13
- ## Topic Guides
14
-
15
- - [Architecture](.agents/architecture.md) - Core components and project structure
16
- - [Testing](.agents/testing.md) - Minitest spec DSL and VCR cassettes
17
- - [Code Style](.agents/code-style.md) - RuboCop, comment, and RDoc conventions
18
- - [Providers](.agents/providers.md) - Adding new LLM provider adapters
19
- - [RBS Inline](.agents/rbs-inline.md) - Type annotations with rbs-inline
20
-
21
13
  ## Commands
22
14
 
23
15
  All wrappers `exec bundle exec …` under the hood.
@@ -0,0 +1,13 @@
1
+ ---
2
+ paths: ["**/*.rb", "**/*.rake", "**/Gemfile"]
3
+ ---
4
+
5
+ # Comments
6
+
7
+ A comment exists to explain a **why** when the code itself cannot — never a **how**, and never a restatement of what the code already says. This bar governs all prose, from inline comments to docstrings.
8
+
9
+ - **Internal and private code** — everything in an application, plus a library's non-exported internals — is self-documenting via clear names and strong types. A comment survives only when it explains something a competent reader cannot recover from the code alone: a non-local constraint, an external-system quirk, a deliberate non-obvious tradeoff. A description of _what_ the code does, or a why that's evident from reading it, gets cut.
10
+ - **A published library's public surface** gets one verb-first sentence per exported symbol ("Serializes the definition to JSON."). An optional second sentence is reserved strictly for a why — a non-obvious constraint or rationale — never a second sentence of how. Needing more than one sentence to say _what_ it does is a smell the symbol does too much.
11
+ - **Types are not prose's job.** Parameters, return values, and field types live in the type system (TypeScript types, rbs-inline `#:` annotations) — never restated in comments that duplicate them.
12
+ - **Markers.** `TODO` / `FIXME` / `HACK` are tracked work and stay; `NOTE` / `REVIEW` meet the same why-bar as any other comment.
13
+ - **No history.** A comment describes the present, never how the code got there — no "was X, now Y", no story of the bug that revealed a constraint. State a still-true constraint in the present tense ("the API returns null for empty results — guard").
@@ -0,0 +1,93 @@
1
+ ---
2
+ paths: ["lib/**/*.rb", "sig/**/*"]
3
+ ---
4
+
5
+ # RBS Inline
6
+
7
+ Type annotations are added directly in Ruby source files using [rbs-inline](https://github.com/soutaro/rbs-inline).
8
+
9
+ ## Required Header
10
+
11
+ Every `lib/**/*.rb` file starts with the two-line header `# frozen_string_literal: true` + `# rbs_inline: enabled` — rbs-inline silently skips any file missing the magic comment on line 2.
12
+
13
+ ## Annotation Conventions
14
+
15
+ The **`#:`** prefix is used — standalone lines above methods (RBS method signatures) or inline on the same line (attributes, constants).
16
+
17
+ `# @rbs` is used **only** for ivar declarations; everything else uses `#:`. The `#:` shorthand on an assignment is a Steep _assertion_ — it types the expression but does **not declare the ivar**. To declare an ivar's type, use a `# @rbs` comment inside the class body:
18
+
19
+ ```ruby
20
+ class Riffer::Agent::Session
21
+ # @rbs @callbacks: Array[^(Riffer::Messages::Base) -> void] # instance ivar
22
+ end
23
+
24
+ module Riffer
25
+ # @rbs self.@config: Riffer::Config? # class/module-level ivar
26
+ end
27
+ ```
28
+
29
+ ## RDoc Conventions
30
+
31
+ **The `#--` stop directive.** Place `#--` on the line immediately before a **standalone** `#:` type annotation. Without it, RDoc treats `#:` as a label-list marker and corrupts the preceding description into a `<pre>` block. Inline `#:` on the same line as code (attributes, constants) does not need it.
32
+
33
+ ```ruby
34
+ # Serializes the agent definition to a transferable JSON payload.
35
+ #--
36
+ #: (Riffer::Agent) -> String
37
+ def serialize(agent)
38
+
39
+ # The agent's display name.
40
+ attr_reader :name #: String
41
+ ```
42
+
43
+ **Raises.** Document a raise **only when it's part of the caller's contract** — something a caller should reasonably anticipate and handle. Skip programmer-error guards and "should never happen" assertions. When the raise condition merely restates the declared `#:` type, phrase it by intent ("Raises Riffer::ArgumentError on an invalid value") rather than re-listing the type union.
44
+
45
+ **Examples.** Include an example only when a **consumer is likely to use the thing themselves** — a public entry point they construct, subclass, or call. Keep them sparing and write them as indented code blocks (2 extra spaces of indent). Usage walkthroughs belong in `docs/`.
46
+
47
+ **Inline code formatting.** Use `+word+` for single-word inline code; for multi-word expressions (spaces, colons, brackets) use `<tt>multi word expression</tt>`.
48
+
49
+ **Internal APIs.** Mark with `# :nodoc:` to exclude from generated documentation.
50
+
51
+ ## Optional-dependency types (consumer-safe signatures)
52
+
53
+ `sig/generated/` ships with the gem and is loaded by downstream projects (`rbs collection` / `rbs -r riffer`). rbs-inline copies a method's `#:` signature **verbatim** into the shipped sig, so **never name an optional-dependency type in a `#:` signature** — `OpenAI::*`, `Anthropic::*`, `Aws::*`, `MCP::*`, `Async::*`, `Zeitwerk::*`, etc. A consumer who installs riffer without that gem would hit `Cannot find type`, because those providers are pluggable and the gems ship no usable RBS of their own.
54
+
55
+ **Workaround — assert the type inside the method body instead.** An inline assertion (`local = arg #: OpenAI::Models::…`) is a Steep-only hint that rbs-inline does **not** emit into the signature. Leave the SDK param/return `untyped` in the `#:` line, keep every riffer/stdlib param and return typed, and recover the SDK type with a body assertion:
56
+
57
+ ```ruby
58
+ #: (untyped) -> String
59
+ def extract_content(response)
60
+ message = response #: Anthropic::Models::Message
61
+ message.content&.first&.text || "" # fully type-checked against the SDK type
62
+ end
63
+
64
+ #: (untyped, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
65
+ def handle_stream_chunk(chunk, state:, yielder:)
66
+ typed = chunk #: OpenAI::Models::Chat::ChatCompletionChunk
67
+ # ...
68
+ end
69
+
70
+ # When the return value IS the SDK object, type the return `untyped` (no body assertion needed):
71
+ #: (Hash[Symbol, untyped]) -> untyped
72
+ def execute_generate(params)
73
+ @client.messages.create(**params)
74
+ end
75
+ ```
76
+
77
+ `test/shipped_signatures_test.rb` enforces this — it fails if any optional-dependency type appears in `sig/generated/` or `sig/manual/`.
78
+
79
+ ### Where stubs and stdlib deps live
80
+
81
+ - `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 (`async.rbs`, `mcp.rbs`, `zeitwerk.rbs`, `openai.rbs`, `anthropic.rbs`, `aws-sdk-core/*` — full stubs for RBS-less gems plus arity patches for the provider SDKs); 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).
82
+ - `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
+ - `sig/manifest.yaml` — declares the **stdlib** RBS the shipped sigs reference (`uri`, `net-http`) so `rbs -r riffer` resolves them.
84
+
85
+ ## Workflow
86
+
87
+ After changing type annotations:
88
+
89
+ 1. Run `bin/rbs` to regenerate `sig/generated/` files
90
+ 2. Commit both the source changes and the generated `.rbs` files
91
+ 3. CI checks for drift between source annotations and committed `.rbs` files
92
+
93
+ Use `bin/rbs-watch` during development to auto-regenerate on file changes.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.40.0"
2
+ ".": "0.42.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ 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.42.0](https://github.com/janeapp/riffer/compare/riffer/v0.41.0...riffer/v0.42.0) (2026-08-25)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * duplicate identifiers among named direct subclasses of Riffer::Tool or Riffer::Agent now raise Riffer::DuplicateIdentifierError on lookup. Agent.find/.all (and the new Tool.find/.all) cover named direct subclasses only - anonymous classes are excluded even with an explicit identifier. Riffer::Helpers::ClassNameConverter is removed; use Riffer::Helpers::Identifier.derive (the separator: keyword is dropped).
14
+
15
+ ### Features
16
+
17
+ * one identifier registry pattern for tools and agents ([#406](https://github.com/janeapp/riffer/issues/406)) ([72fa7d6](https://github.com/janeapp/riffer/commit/72fa7d6377f903e4a88f9f5ec0e27e51b46dc293))
18
+
19
+ ## [0.41.0](https://github.com/janeapp/riffer/compare/riffer/v0.40.0...riffer/v0.41.0) (2026-08-14)
20
+
21
+
22
+ ### ⚠ BREAKING CHANGES
23
+
24
+ * Inference providers no longer support extra configuration parameters (e.g. timeouts). Instead, a provider client object can be defined in the riffer config directly. Credential and endpoint parameters are still supported.
25
+
26
+ ### Features
27
+
28
+ * config-injected provider clients, retire provider_options ([#391](https://github.com/janeapp/riffer/issues/391)) ([72fa0e3](https://github.com/janeapp/riffer/commit/72fa0e32ff6cfeee29bae9e2618d001cbc387d99))
29
+
8
30
  ## [0.40.0](https://github.com/janeapp/riffer/compare/riffer/v0.39.0...riffer/v0.40.0) (2026-08-12)
9
31
 
10
32
 
data/README.md CHANGED
@@ -45,31 +45,31 @@ puts agent.generate('Hello world')
45
45
 
46
46
  ## Documentation
47
47
 
48
- Comprehensive guides live at [docs.riffer.ai](https://docs.riffer.ai):
49
-
50
- - [Overview](https://docs.riffer.ai/guides/overview/) - Core concepts and architecture
51
- - [Getting Started](https://docs.riffer.ai/guides/getting-started/) - Installation and first steps
52
- - [Agents](https://docs.riffer.ai/guides/agents/) - Defining and configuring agents
53
- - [Agent Lifecycle](https://docs.riffer.ai/guides/agent-lifecycle/) - Generate, stream, and responses
54
- - [Agent Loop](https://docs.riffer.ai/guides/agent-loop/) - Tool execution flow and stopping
55
- - [Tools](https://docs.riffer.ai/guides/tools/) - Creating tools for agents
56
- - [Advanced Tools](https://docs.riffer.ai/guides/advanced-tools/) - Timeouts, runtime, and registration
57
- - [Messages](https://docs.riffer.ai/guides/messages/) - Message types and formats
58
- - [Stream Events](https://docs.riffer.ai/guides/stream-events/) - Streaming responses
59
- - [Configuration](https://docs.riffer.ai/guides/configuration/) - Framework configuration
60
- - [Evals](https://docs.riffer.ai/guides/evals/) - Evaluating agent quality
61
- - [Guardrails](https://docs.riffer.ai/guides/guardrails/) - Input/output validation
62
- - [Skills](https://docs.riffer.ai/guides/skills/) - Packaged agent capabilities
63
- - [MCP](https://docs.riffer.ai/guides/mcp/) - Integrating third-party MCP servers
64
- - [Serialization](https://docs.riffer.ai/guides/serialization/) - Persisting and transferring agent definitions
65
- - [Tracing](https://docs.riffer.ai/guides/tracing/) - OpenTelemetry span contract and host wiring
66
- - [Providers](https://docs.riffer.ai/guides/providers/overview/) - LLM provider adapters
48
+ Comprehensive guides live at [riffer.ai](https://riffer.ai):
49
+
50
+ - [Overview](https://riffer.ai/guides/overview/) - Core concepts and architecture
51
+ - [Getting Started](https://riffer.ai/guides/getting-started/) - Installation and first steps
52
+ - [Agents](https://riffer.ai/guides/agents/) - Defining and configuring agents
53
+ - [Agent Lifecycle](https://riffer.ai/guides/agent-lifecycle/) - Generate, stream, and responses
54
+ - [Agent Loop](https://riffer.ai/guides/agent-loop/) - Tool execution flow and stopping
55
+ - [Tools](https://riffer.ai/guides/tools/) - Creating tools for agents
56
+ - [Advanced Tools](https://riffer.ai/guides/advanced-tools/) - Timeouts, runtime, and registration
57
+ - [Messages](https://riffer.ai/guides/messages/) - Message types and formats
58
+ - [Stream Events](https://riffer.ai/guides/stream-events/) - Streaming responses
59
+ - [Configuration](https://riffer.ai/guides/configuration/) - Framework configuration
60
+ - [Evals](https://riffer.ai/guides/evals/) - Evaluating agent quality
61
+ - [Guardrails](https://riffer.ai/guides/guardrails/) - Input/output validation
62
+ - [Skills](https://riffer.ai/guides/skills/) - Packaged agent capabilities
63
+ - [MCP](https://riffer.ai/guides/mcp/) - Integrating third-party MCP servers
64
+ - [Serialization](https://riffer.ai/guides/serialization/) - Persisting and transferring agent definitions
65
+ - [Tracing](https://riffer.ai/guides/tracing/) - OpenTelemetry span contract and host wiring
66
+ - [Providers](https://riffer.ai/guides/providers/overview/) - LLM provider adapters
67
67
 
68
68
  The guide sources are in the [docs](docs/) directory.
69
69
 
70
70
  ### API Reference
71
71
 
72
- The full API reference is published at [docs.riffer.ai/api](https://docs.riffer.ai/api/). Preview the site locally with:
72
+ The full API reference is published at [riffer.ai/api](https://riffer.ai/api/). Preview the site locally with:
73
73
 
74
74
  ```bash
75
75
  bin/rake docs:serve
@@ -140,7 +140,3 @@ All notable changes to this project are documented in `CHANGELOG.md`.
140
140
  ## License
141
141
 
142
142
  Licensed under the MIT License. See `LICENSE.txt` for details.
143
-
144
- ## Maintainers
145
-
146
- - Jake Bottrall - https://github.com/bottrall
data/docs/AGENTS.md CHANGED
@@ -128,17 +128,6 @@ end
128
128
 
129
129
  Loads tools from registered [MCP](MCP.md) servers by tag. Like `uses_tools`, **`use_mcp` is not inherited**—add it on each subclass that should include MCP tools.
130
130
 
131
- ### provider_options
132
-
133
- Passes options to the provider client:
134
-
135
- ```ruby
136
- class MyAgent < Riffer::Agent
137
- model 'openai/gpt-5-mini'
138
- provider_options api_key: ENV['CUSTOM_OPENAI_KEY']
139
- end
140
- ```
141
-
142
131
  ### model_options
143
132
 
144
133
  Passes options to each LLM request:
@@ -317,6 +306,27 @@ agent.generate('Hello')
317
306
 
318
307
  When `config:` is supplied, the class-level configuration is ignored for that instance.
319
308
 
309
+ ## Looking Up Agents
310
+
311
+ Look up an agent by identifier with `Riffer::Agent.find`, or list every agent with `Riffer::Agent.all`:
312
+
313
+ ```ruby
314
+ class SupportAgent < Riffer::Agent
315
+ model 'openai/gpt-5-mini'
316
+ end
317
+
318
+ Riffer::Agent.find('support_agent') # => SupportAgent
319
+ Riffer::Agent.find(:support_agent) # symbols work too
320
+ Riffer::Agent.find('missing') # => nil
321
+ Riffer::Agent.all # => [SupportAgent, ...]
322
+ ```
323
+
324
+ Only **named direct subclasses** are found:
325
+
326
+ - Grandchildren are not visible to a grandparent's `find` or `all`. If your app defines an intermediate base class (`class ApplicationAgent < Riffer::Agent`), call `find`/`all` on the intermediate class to look up its subclasses.
327
+ - Anonymous classes (`Class.new(Riffer::Agent)`) are never findable, even when they set an explicit `identifier`.
328
+ - Two subclasses sharing an identifier raise `Riffer::DuplicateIdentifierError` at the first lookup.
329
+
320
330
  ## Per-Call Tags
321
331
 
322
332
  `#generate` and `#stream` accept an optional `tags:` hash — a flat map of attribution labels scoped to that single call (for cost/usage attribution, filtering audit logs, slicing telemetry). It is **per-call only**.
@@ -359,10 +369,10 @@ Riffer does not validate tag count, key/value length, or charset — it forwards
359
369
 
360
370
  ## Expand Your Agent
361
371
 
362
- | Goal | Feature | Guide |
363
- | ----------------------------- | ----------------- | -------------------------------------------------------------------- |
372
+ | Goal | Feature | Guide |
373
+ | ----------------------------- | ----------------- | ----------------------------------------------------------------- |
364
374
  | Call APIs or run functions | Tools | [Tools](TOOLS.md) |
365
- | Return structured JSON | Structured Output | [structured_output](#structured_output) |
375
+ | Return structured JSON | Structured Output | [structured_output](#structured_output) |
366
376
  | Validate or filter content | Guardrails | [Guardrails](GUARDRAILS.md) |
367
377
  | Measure output quality | Evals | [Evals](EVALS.md) |
368
378
  | Add packaged capabilities | Skills | [Skills](SKILLS.md) |
@@ -347,7 +347,7 @@ See [Messages — Structured Output on Messages](MESSAGES.md#structured-output-o
347
347
 
348
348
  ### find
349
349
 
350
- Find an agent class by identifier:
350
+ Find an agent class by identifier (named direct subclasses only — see [Agents — Looking Up Agents](AGENTS.md#looking-up-agents)):
351
351
 
352
352
  ```ruby
353
353
  agent_class = Riffer::Agent.find('my_agent')
@@ -356,7 +356,7 @@ agent = agent_class.new
356
356
 
357
357
  ### all
358
358
 
359
- List all agent subclasses:
359
+ List all registered agent subclasses (named direct subclasses only):
360
360
 
361
361
  ```ruby
362
362
  Riffer::Agent.all.each do |agent_class|
@@ -9,12 +9,15 @@ Use `Riffer.configure` to set up provider credentials:
9
9
  ```ruby
10
10
  Riffer.configure do |config|
11
11
  config.openai.api_key = ENV['OPENAI_API_KEY']
12
+ config.openai.base_url = ENV['OPENAI_BASE_URL'] # Optional — gateways, proxies
12
13
  config.amazon_bedrock.region = 'us-east-1'
13
14
  config.amazon_bedrock.api_token = ENV['BEDROCK_API_TOKEN']
14
15
  config.anthropic.api_key = ENV['ANTHROPIC_API_KEY']
15
16
  end
16
17
  ```
17
18
 
19
+ Providers take no constructor arguments — these settings are the only way to give a provider its credentials.
20
+
18
21
  ## Accessing Configuration
19
22
 
20
23
  Access the current configuration via `Riffer.config`:
@@ -34,6 +37,66 @@ Riffer.config.anthropic.api_key
34
37
 
35
38
  For provider credentials and setup, see the individual [Provider guides](providers/PROVIDERS.md).
36
39
 
40
+ ### Provider Clients
41
+
42
+ Out of the box, each provider builds a default SDK client from its configured credentials. For anything beyond credentials — timeouts, retries, proxies, gateways — assign your own client to `config.<provider>.client`:
43
+
44
+ ```ruby
45
+ Riffer.configure do |config|
46
+ config.openai.client = OpenAI::Client.new(
47
+ api_key: ENV['OPENAI_API_KEY'],
48
+ timeout: 30,
49
+ max_retries: 4
50
+ )
51
+ config.gemini.client = Riffer::Providers::Gemini::Client.new(
52
+ api_key: ENV['GEMINI_API_KEY'],
53
+ read_timeout: 120
54
+ )
55
+ end
56
+ ```
57
+
58
+ Every provider accepts a client instance or a `Proc` returning one:
59
+
60
+ | Provider | Setting | Default client built from credentials |
61
+ | -------------- | ------------------------------ | ------------------------------------------------------------- |
62
+ | OpenAI | `config.openai.client` | `OpenAI::Client` |
63
+ | Azure OpenAI | `config.azure_openai.client` | `OpenAI::Client` (with the Azure endpoint as `base_url`) |
64
+ | Anthropic | `config.anthropic.client` | `Anthropic::Client` |
65
+ | Amazon Bedrock | `config.amazon_bedrock.client` | `Aws::BedrockRuntime::Client` |
66
+ | Gemini | `config.gemini.client` | `Riffer::Providers::Gemini::Client` (riffer-owned, see below) |
67
+ | OpenRouter | `config.openrouter.client` | `OpenAI::Client` (pinned to the OpenRouter endpoint) |
68
+
69
+ A `Proc` takes **no arguments** and is resolved on **every LLM call**, never cached by riffer — memoize inside the Proc when construction is expensive. This makes the Proc the right tool for:
70
+
71
+ - **Fork safety** (Puma clustered, Sidekiq swarm): a client built at boot holds sockets that break across `fork`; build (and cache) per process instead.
72
+ - **Expiring credentials** (Azure AD tokens, STS-vended keys): re-resolve before they go stale.
73
+
74
+ ```ruby
75
+ Riffer.configure do |config|
76
+ # Fork-safe shared client: one per process, built on first use after fork.
77
+ config.anthropic.client = -> {
78
+ ClientRegistry.anthropic_for(Process.pid)
79
+ }
80
+
81
+ # Re-resolved before the token goes stale.
82
+ config.azure_openai.client = -> {
83
+ OpenAI::Client.new(api_key: AzureAd.current_token, base_url: ENV['AZURE_OPENAI_ENDPOINT'])
84
+ }
85
+ end
86
+ ```
87
+
88
+ Because the Proc receives no arguments, it can only vary the client by process-wide state — it cannot route per agent or per request. Client selection is a global concern; to talk to different accounts or endpoints from different agents, register a provider subclass with its own config (see [Multiple Configurations](#multiple-configurations)).
89
+
90
+ A configured client always wins over configured credentials: once `config.<provider>.client` is set, the credential members are unused, since riffer no longer builds the client.
91
+
92
+ ### Falling through to the SDK
93
+
94
+ A credential you leave unset in riffer is omitted from the default client rather than passed as `nil`, so each vendor SDK still applies its own resolution. `OPENAI_API_KEY` / `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY`, and the AWS chains — region from `AWS_REGION` / `AWS_DEFAULT_REGION` / shared config, credentials from the environment, the shared credentials file, or an instance/task IAM role — all work with no riffer configuration at all.
95
+
96
+ Two providers deliberately opt out: `OpenRouter` and `AzureOpenAI` borrow `OpenAI::Client` to reach a **different** vendor, so they always pass their credential and endpoint explicitly. Falling through would let the OpenAI SDK pick up `OPENAI_API_KEY` / `OPENAI_BASE_URL` and send an OpenAI credential to `openrouter.ai` or your Azure endpoint. With nothing configured they raise instead — set `config.openrouter.api_key` / `OPENROUTER_API_KEY`, or `config.azure_openai.api_key` and `.endpoint` / `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT`.
97
+
98
+ The Gemini provider has no vendor SDK, so riffer ships its own transport: `Riffer::Providers::Gemini::Client` exposes `base_url`, `open_timeout`, `read_timeout`, `write_timeout`, and `proxy_address`/`proxy_port`. Anything implementing its two-method contract (`post`, `post_stream`) can be assigned to `config.gemini.client` — see [Gemini](providers/GEMINI.md).
99
+
37
100
  ### MCP (Model Context Protocol)
38
101
 
39
102
  Optional settings for [MCP server integrations](MCP.md):
@@ -113,10 +176,10 @@ Riffer.configure do |config|
113
176
  end
114
177
  ```
115
178
 
116
- | Option | Description |
117
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
118
- | `enabled` | The kill switch, consulted on every span — flipping it at runtime takes effect immediately, short-circuiting to a no-op ahead of the backend. Accepts booleans or `'true'`/`'false'`/`'1'`/`'0'`. Defaults to `true`. |
119
- | `capture_messages` | Opt-in capture of full message content on LLM-call spans (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`) as GenAI-semconv JSON. Defaults to `false` — message content routinely carries sensitive data. File attachments serialize as metadata-only stubs (media type and name, never bytes), and riffer applies no size limit of its own — cap oversized attributes with the OTEL SDK attribute length limits. |
179
+ | Option | Description |
180
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | `enabled` | The kill switch, consulted on every span — flipping it at runtime takes effect immediately, short-circuiting to a no-op ahead of the backend. Accepts booleans or `'true'`/`'false'`/`'1'`/`'0'`. Defaults to `true`. |
182
+ | `capture_messages` | Opt-in capture of full message content on LLM-call spans (`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`) as GenAI-semconv JSON. Defaults to `false` — message content routinely carries sensitive data. File attachments serialize as metadata-only stubs (media type and name, never bytes), and riffer applies no size limit of its own — cap oversized attributes with the OTEL SDK attribute length limits. |
120
183
  | `backend` | The backend riffer routes spans through. Assign `Riffer::Tracing::Otel.build` (pass `provider:` to override the global tracer provider — e.g. an in-memory provider in tests), or any object satisfying the duck-typed contract (`in_span` / `current_context` / `with_context`) to route into a non-OTEL system (e.g. Datadog APM). Defaults to `nil` — a no-op. Raises `Riffer::ArgumentError` unless the value is `nil` or responds to `in_span`. See [Tracing → Routing to a non-OpenTelemetry backend](TRACING.md#routing-to-a-non-opentelemetry-backend). |
121
184
 
122
185
  ### Pricing
@@ -200,19 +263,6 @@ There is no per-call override and no customizable placeholder. Callers needing f
200
263
 
201
264
  Override global configuration at the agent level:
202
265
 
203
- ### provider_options
204
-
205
- Pass options directly to the provider client:
206
-
207
- ```ruby
208
- class MyAgent < Riffer::Agent
209
- model 'openai/gpt-5-mini'
210
-
211
- # Override API key for this agent only
212
- provider_options api_key: ENV['CUSTOM_OPENAI_KEY']
213
- end
214
- ```
215
-
216
266
  ### model_options
217
267
 
218
268
  Pass options to each LLM request:
@@ -305,17 +355,51 @@ end
305
355
 
306
356
  ## Multiple Configurations
307
357
 
308
- For different environments or use cases, use agent-level overrides:
358
+ Provider credentials and clients are **global**, resolved per process. For different environments, branch at boot:
359
+
360
+ ```ruby
361
+ Riffer.configure do |config|
362
+ config.openai.client = if Rails.env.production?
363
+ OpenAI::Client.new(api_key: ENV['PRODUCTION_OPENAI_KEY'], max_retries: 4)
364
+ else
365
+ OpenAI::Client.new(api_key: ENV['DEV_OPENAI_KEY'], timeout: 10)
366
+ end
367
+ end
368
+ ```
369
+
370
+ When a single process has to reach two different accounts or endpoints, give the second one its own provider class and config, then register it under its own identifier:
371
+
372
+ ```ruby
373
+ class InternalOpenAI < Riffer::Providers::OpenAI
374
+ InternalConfig = Struct.new(:api_key, :base_url, :client)
375
+
376
+ def self.config
377
+ @config ||= InternalConfig.new(ENV.fetch('INTERNAL_OPENAI_KEY'), ENV.fetch('INTERNAL_GATEWAY'))
378
+ end
379
+
380
+ private
381
+
382
+ def global_client
383
+ self.class.config.client
384
+ end
385
+
386
+ def build_client
387
+ ::OpenAI::Client.new(api_key: self.class.config.api_key, base_url: self.class.config.base_url)
388
+ end
389
+ end
390
+
391
+ Riffer::Providers::Repository.register(:internal_openai) { InternalOpenAI }
392
+ ```
393
+
394
+ Agents then select it by model prefix (`model 'internal_openai/gpt-5-mini'`), and the two accounts never interfere. What _can_ vary per agent is the model and the generation parameters:
309
395
 
310
396
  ```ruby
311
397
  class ProductionAgent < Riffer::Agent
312
398
  model 'openai/gpt-5-mini'
313
- provider_options api_key: ENV['PRODUCTION_OPENAI_KEY']
314
399
  end
315
400
 
316
401
  class DevelopmentAgent < Riffer::Agent
317
402
  model 'openai/gpt-5-mini'
318
- provider_options api_key: ENV['DEV_OPENAI_KEY']
319
- model_options temperature: 0.0 # Deterministic for testing
403
+ model_options temperature: 0.0 # Deterministic for testing
320
404
  end
321
405
  ```
@@ -1,6 +1,6 @@
1
1
  # Serialization
2
2
 
3
- `Riffer::Agent::Serializer` turns a **resolved agent** into a self-contained, provider-neutral data hash (`to_h`) and reconstructs a **runnable agent** from that hash (`from_h`). Use it to persist agent definitions outside of code, or to transfer them across a process/service boundary.
3
+ `Riffer::Agent::Serializer` turns a **resolved agent** into a provider-neutral data hash (`to_h`) and rebuilds an agent from that hash (`from_h`). The hash carries the agent **definition** — not the credentials or client it needs to reach a provider (see [Secrets](#secrets)). Use it to persist agent definitions outside of code, or to transfer them across a process/service boundary.
4
4
 
5
5
  You normally reach it through the delegators on `Riffer::Agent`:
6
6
 
@@ -43,7 +43,6 @@ The session is used **as-is**: the rebuilt agent does not prepend anything to it
43
43
  model: "openai/gpt-4o", # resolved "provider/model" string
44
44
  instructions: "You are…", # resolved system prompt
45
45
  model_options: { temperature: 0.2 },
46
- provider_options: { … }, # see the secrets warning below
47
46
  max_steps: 8, # integer; -1 = unlimited (see below)
48
47
  structured_output: { type: "object", … }, # JSON Schema, or null
49
48
  tools: [ { name:, description:, parameters_schema:, timeout: }, … ]
@@ -101,7 +100,9 @@ A finite integer round-trips as-is; a hash missing the key falls back to the def
101
100
 
102
101
  ## Secrets
103
102
 
104
- `provider_options` and `model_options` **ride on the wire as plain data** — they are part of the hash and _will_ transfer. Prefer configuring API keys via environment/global provider configuration rather than `provider_options`. **Never serialize an agent whose options carry sensitive values** and if a serialized definition ever does, handle it as a secret (encrypt it, keep it out of logs).
103
+ `model_options` **rides on the wire as plain data** — it is part of the hash and _will_ transfer. Keep sensitive values out of it; credentials live in global provider configuration and never serialize.
104
+
105
+ The hash describes the agent, not how to authenticate as one. The destination process configures the provider itself — `config.<provider>.client`, or the credentials riffer builds a default client from. Clients resolve lazily on the first LLM call, so `from_h` succeeds in a process with no provider configuration; the `generate` is what fails. See [Configuration](CONFIGURATION.md).
105
106
 
106
107
  ## What does **not** transfer
107
108
 
data/docs/TOOLS.md CHANGED
@@ -108,7 +108,7 @@ Options:
108
108
  | `String` | `string` |
109
109
  | `Integer` | `integer` |
110
110
  | `Float` | `number` |
111
- | `Riffer::Params::Boolean` | `boolean` |
111
+ | `Riffer::Params::Boolean` | `boolean` |
112
112
  | `TrueClass` / `FalseClass` | `boolean` |
113
113
  | `Array` | `array` |
114
114
  | `Hash` | `object` |
@@ -143,6 +143,23 @@ class CreateOrderTool < Riffer::Tool
143
143
  end
144
144
  ```
145
145
 
146
+ ## Looking Up Tools
147
+
148
+ Look up a tool by identifier with `Riffer::Tool.find`, or list every tool with `Riffer::Tool.all`:
149
+
150
+ ```ruby
151
+ Riffer::Tool.find('kb_search') # => SearchTool
152
+ Riffer::Tool.find(:kb_search) # symbols work too
153
+ Riffer::Tool.find('missing') # => nil
154
+ Riffer::Tool.all # => [SearchTool, ...]
155
+ ```
156
+
157
+ Only **named direct subclasses** are found:
158
+
159
+ - Grandchildren are not visible to a grandparent's `find` or `all`. If your app defines an intermediate base class (`class ApplicationTool < Riffer::Tool`), call `find`/`all` on the intermediate class to look up its subclasses.
160
+ - Anonymous classes (`Class.new(Riffer::Tool)`) are never findable, even when they set an explicit `identifier`.
161
+ - Two subclasses sharing an identifier raise `Riffer::DuplicateIdentifierError` at the first lookup.
162
+
146
163
  ## The call Method
147
164
 
148
165
  Every tool must implement the `call` method and return a `Riffer::Tools::Response`:
@@ -22,6 +22,8 @@ Riffer.configure do |config|
22
22
  end
23
23
  ```
24
24
 
25
+ `region` resolves in order: `Riffer.config.amazon_bedrock.region` → the AWS SDK's own resolution (`AWS_REGION`, `AWS_DEFAULT_REGION`, shared config). Leaving it unset in riffer defers to the SDK rather than failing.
26
+
25
27
  ### Bearer Token Authentication
26
28
 
27
29
  For API token authentication:
@@ -33,15 +35,22 @@ Riffer.configure do |config|
33
35
  end
34
36
  ```
35
37
 
36
- Or per-agent:
38
+ ### Custom client
39
+
40
+ AWS auth beyond the bearer token or default credential chain (profiles, STS, IRSA), plus retries, timeouts, and endpoints, is configured on your own `Aws::BedrockRuntime::Client`:
37
41
 
38
42
  ```ruby
39
- class MyAgent < Riffer::Agent
40
- model 'amazon_bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0'
41
- provider_options region: 'us-west-2', api_token: ENV['BEDROCK_API_TOKEN']
43
+ Riffer.configure do |config|
44
+ config.amazon_bedrock.client = Aws::BedrockRuntime::Client.new(
45
+ region: 'us-east-1',
46
+ credentials: Aws::AssumeRoleCredentials.new(role_arn: ENV['BEDROCK_ROLE_ARN'], role_session_name: 'riffer'),
47
+ retry_limit: 5
48
+ )
42
49
  end
43
50
  ```
44
51
 
52
+ The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
53
+
45
54
  ## Supported Models
46
55
 
47
56
  Use Bedrock model IDs in the `amazon_bedrock/model` format:
@@ -168,10 +177,7 @@ The provider converts Riffer messages to Bedrock format:
168
177
  ## Direct Provider Usage
169
178
 
170
179
  ```ruby
171
- provider = Riffer::Providers::AmazonBedrock.new(
172
- region: 'us-east-1',
173
- api_token: ENV['BEDROCK_API_TOKEN'] # Optional
174
- )
180
+ provider = Riffer::Providers::AmazonBedrock.new
175
181
 
176
182
  response = provider.generate_text(
177
183
  prompt: "Hello!",
@@ -20,15 +20,19 @@ Riffer.configure do |config|
20
20
  end
21
21
  ```
22
22
 
23
- Or per-agent:
23
+ For anything beyond the API key — timeouts, retries, proxies — supply your own `Anthropic::Client`:
24
24
 
25
25
  ```ruby
26
- class MyAgent < Riffer::Agent
27
- model 'anthropic/claude-haiku-4-5-20251001'
28
- provider_options api_key: ENV['ANTHROPIC_API_KEY']
26
+ Riffer.configure do |config|
27
+ config.anthropic.client = Anthropic::Client.new(
28
+ api_key: ENV['ANTHROPIC_API_KEY'],
29
+ timeout: 60
30
+ )
29
31
  end
30
32
  ```
31
33
 
34
+ The setting accepts a client instance or a no-argument `Proc`, resolved on every LLM call — see [Configuration → Provider Clients](../CONFIGURATION.md#provider-clients).
35
+
32
36
  ## Supported Models
33
37
 
34
38
  Use Anthropic model IDs in the `anthropic/model` format:
@@ -224,9 +228,7 @@ The provider converts Riffer messages to Anthropic format:
224
228
  ## Direct Provider Usage
225
229
 
226
230
  ```ruby
227
- provider = Riffer::Providers::Anthropic.new(
228
- api_key: ENV['ANTHROPIC_API_KEY']
229
- )
231
+ provider = Riffer::Providers::Anthropic.new
230
232
 
231
233
  response = provider.generate_text(
232
234
  prompt: "Hello!",