riffer 0.41.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 (39) 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/{.agents → .claude/rules}/rbs-inline.md +28 -104
  5. data/.release-please-manifest.json +1 -1
  6. data/CHANGELOG.md +11 -0
  7. data/docs/AGENTS.md +24 -3
  8. data/docs/AGENT_LIFECYCLE.md +2 -2
  9. data/docs/TOOLS.md +18 -1
  10. data/lib/riffer/agent/run.rb +2 -4
  11. data/lib/riffer/agent.rb +3 -17
  12. data/lib/riffer/guardrail.rb +1 -1
  13. data/lib/riffer/helpers/identifier.rb +41 -0
  14. data/lib/riffer/providers/anthropic.rb +4 -0
  15. data/lib/riffer/providers/base.rb +4 -1
  16. data/lib/riffer/registrable.rb +81 -0
  17. data/lib/riffer/tool.rb +1 -0
  18. data/lib/riffer/tools/toolable.rb +2 -3
  19. data/lib/riffer/version.rb +1 -1
  20. data/lib/riffer.rb +3 -0
  21. data/sig/generated/riffer/agent.rbs +2 -12
  22. data/sig/generated/riffer/helpers/identifier.rbs +19 -0
  23. data/sig/generated/riffer/providers/base.rbs +2 -0
  24. data/sig/generated/riffer/registrable.rbs +51 -0
  25. data/sig/generated/riffer/tool.rbs +2 -0
  26. data/sig/generated/riffer/tools/toolable.rbs +3 -1
  27. data/sig/generated/riffer.rbs +4 -0
  28. data/sig/manual/riffer/agent.rbs +7 -0
  29. data/sig/manual/riffer/helpers/identifier.rbs +5 -0
  30. data/sig/manual/riffer/tool.rbs +7 -0
  31. metadata +11 -11
  32. data/.agents/architecture.md +0 -265
  33. data/.agents/code-style.md +0 -110
  34. data/.agents/providers.md +0 -54
  35. data/.agents/testing.md +0 -60
  36. data/CLAUDE.md +0 -1
  37. data/lib/riffer/helpers/class_name_converter.rb +0 -22
  38. data/sig/generated/riffer/helpers/class_name_converter.rbs +0 -12
  39. 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: fd309de6be56bd5fbe778bbf277b9125dd63d4ce7b360c3e5e85554a944cb81d
4
- data.tar.gz: 965daf888849a0386b2826af572fd95d0a7fb8a4ac6f9f91788acb0254705f8e
3
+ metadata.gz: eabed1a1a6844787d92f31be0a72e210117005c19b4dd5e021a905121f83e8e1
4
+ data.tar.gz: 4cc6b6bde7e4b474d7ca6fb2672a3e115d4584b096f1de5d967a2b1a751f49ba
5
5
  SHA512:
6
- metadata.gz: deedbd28b2c79faceea167422fb69147cb2453d7e06df48c46c2eb1e601068de7b9018026675a973f6de6036373d324d5120fc43269e863dd43ce39b88ee7ea4
7
- data.tar.gz: 2007cc98bd5955ae2db4776e72551d546038deea60be3e397a5838e028cd2901465f2fe9c00d944b18e4ce9508599bcd7603ad61e0e75efaa8264793461cf283
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").
@@ -1,128 +1,52 @@
1
+ ---
2
+ paths: ["lib/**/*.rb", "sig/**/*"]
3
+ ---
4
+
1
5
  # RBS Inline
2
6
 
3
7
  Type annotations are added directly in Ruby source files using [rbs-inline](https://github.com/soutaro/rbs-inline).
4
8
 
5
- ## Magic Comment
6
-
7
- rbs-inline only processes a file when `rbs_inline: enabled` is present on line 2. It ships as part of the required header on every `lib/**/*.rb` file — see [Required Header](code-style.md#required-header).
9
+ ## Required Header
8
10
 
9
- ## Annotation Syntax
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.
10
12
 
11
- The **`#:`** prefix is used — standalone lines above methods (type signatures) or inline on the same line (attributes, constants).
13
+ ## Annotation Conventions
12
14
 
13
- ### Method Parameters and Return Types
15
+ The **`#:`** prefix is used — standalone lines above methods (RBS method signatures) or inline on the same line (attributes, constants).
14
16
 
15
- Use a single `#:` line above the method with the RBS method signature:
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:
16
18
 
17
19
  ```ruby
18
- #: (String, Integer) -> bool
19
- def valid?(name, age)
20
- ```
20
+ class Riffer::Agent::Session
21
+ # @rbs @callbacks: Array[^(Riffer::Messages::Base) -> void] # instance ivar
22
+ end
21
23
 
22
- #### Parameter Mapping
24
+ module Riffer
25
+ # @rbs self.@config: Riffer::Config? # class/module-level ivar
26
+ end
27
+ ```
23
28
 
24
- | Ruby param | RBS signature |
25
- | ---------------------------- | ------------------------ |
26
- | `def foo(x)` | `(Type)` |
27
- | `def foo(x = nil)` | `(?Type?)` |
28
- | `def foo(x = val)` | `(?Type)` |
29
- | `def foo(x:)` | `(x: Type)` |
30
- | `def foo(x: nil)` | `(?x: Type?)` |
31
- | `def foo(x: val)` | `(?x: Type)` |
32
- | `def foo(*args)` | `(*untyped)` |
33
- | `def foo(**kwargs)` | `(**untyped)` |
34
- | `def foo(&block)` (required) | `() { (Type) -> void }` |
35
- | `def foo(&block)` (optional) | `() ?{ (Type) -> void }` |
36
- | `def foo(...)` | `(*untyped, **untyped)` |
29
+ ## RDoc Conventions
37
30
 
38
- #### Examples
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.
39
32
 
40
33
  ```ruby
41
- # No parameters
42
- #: () -> String
43
- def name
44
-
45
- # Positional parameters
46
- #: (String, Integer) -> bool
47
- def valid?(name, age)
48
-
49
- # Optional positional parameter
50
- #: (?String?) -> String
51
- def self.identifier(value = nil)
52
-
53
- # Required keyword parameters
54
- #: (input: String, output: String) -> Riffer::Evals::Result
55
- def evaluate(input:, output:)
56
-
57
- # Mixed keyword parameters (required + optional)
58
- #: (input: String, output: String, ?context: Hash[Symbol, untyped]?) -> Riffer::Evals::Result
59
- def evaluate(input:, output:, context: nil)
60
-
61
- # Positional + keyword parameters
62
- #: (String, ?context: Hash[Symbol, untyped]?) -> String
63
- def generate(prompt, context: nil)
64
-
65
- # Splat/double-splat
66
- #: (**untyped) -> void
67
- def initialize(**options)
68
-
69
- # Forward arguments
70
- #: (*untyped, **untyped) -> String
71
- def self.generate(...)
72
-
73
- # Block parameter (required)
74
- #: () { (Riffer::Messages::Base) -> void } -> self
75
- def on_message(&block)
34
+ # Serializes the agent definition to a transferable JSON payload.
35
+ #--
36
+ #: (Riffer::Agent) -> String
37
+ def serialize(agent)
76
38
 
77
- # Block parameter (optional)
78
- #: () ?{ (Riffer::Config) -> void } -> void
79
- def configure(&block)
80
- ```
81
-
82
- ### Attributes
83
-
84
- ```ruby
39
+ # The agent's display name.
85
40
  attr_reader :name #: String
86
- attr_reader :items #: Array[String]
87
41
  ```
88
42
 
89
- ### Constants
90
-
91
- ```ruby
92
- VERSION = "1.0.0" #: String
93
- DEFAULTS = {}.freeze #: Hash[Symbol, untyped]
94
- ```
95
-
96
- ### Instance variables
97
-
98
- The `#:` shorthand on an assignment is a Steep _assertion_ — it types the expression but does
99
- **not declare the ivar**. To declare an ivar's type, use a `# @rbs` comment inside the class
100
- body. `# @rbs` is used **only** for ivar declarations in this codebase; everything else uses
101
- `#:`.
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.
102
44
 
103
- ```ruby
104
- class Riffer::Agent::Session
105
- # @rbs @callbacks: Array[^(Riffer::Messages::Base) -> void] # instance ivar
106
- end
107
-
108
- module Riffer
109
- # @rbs self.@config: Riffer::Config? # class/module-level ivar
110
- end
111
- ```
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/`.
112
46
 
113
- ## Common Type Patterns
47
+ **Inline code formatting.** Use `+word+` for single-word inline code; for multi-word expressions (spaces, colons, brackets) use `<tt>multi word expression</tt>`.
114
48
 
115
- | Pattern | Meaning |
116
- | ------------------------- | --------------------------- |
117
- | `String?` | Optional (String or nil) |
118
- | `(String \| Integer)` | Union type |
119
- | `Array[String]` | Typed array |
120
- | `Hash[Symbol, untyped]` | Typed hash |
121
- | `^(String) -> void` | Block/proc type |
122
- | `singleton(Riffer::Tool)` | Class object (not instance) |
123
- | `bool` | Boolean (true or false) |
124
- | `untyped` | Any type |
125
- | `void` | No meaningful return |
49
+ **Internal APIs.** Mark with `# :nodoc:` to exclude from generated documentation.
126
50
 
127
51
  ## Optional-dependency types (consumer-safe signatures)
128
52
 
@@ -155,7 +79,7 @@ end
155
79
  ### Where stubs and stdlib deps live
156
80
 
157
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).
158
- - `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 "Instance variables"). SDK-typed signatures can't ship, so they go in `_private/riffer/providers/` (the narrowed `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`).
159
83
  - `sig/manifest.yaml` — declares the **stdlib** RBS the shipped sigs reference (`uri`, `net-http`) so `rbs -r riffer` resolves them.
160
84
 
161
85
  ## Workflow
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.41.0"
2
+ ".": "0.42.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ 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
+
8
19
  ## [0.41.0](https://github.com/janeapp/riffer/compare/riffer/v0.40.0...riffer/v0.41.0) (2026-08-14)
9
20
 
10
21
 
data/docs/AGENTS.md CHANGED
@@ -306,6 +306,27 @@ agent.generate('Hello')
306
306
 
307
307
  When `config:` is supplied, the class-level configuration is ignored for that instance.
308
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
+
309
330
  ## Per-Call Tags
310
331
 
311
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**.
@@ -348,10 +369,10 @@ Riffer does not validate tag count, key/value length, or charset — it forwards
348
369
 
349
370
  ## Expand Your Agent
350
371
 
351
- | Goal | Feature | Guide |
352
- | ----------------------------- | ----------------- | -------------------------------------------------------------------- |
372
+ | Goal | Feature | Guide |
373
+ | ----------------------------- | ----------------- | ----------------------------------------------------------------- |
353
374
  | Call APIs or run functions | Tools | [Tools](TOOLS.md) |
354
- | Return structured JSON | Structured Output | [structured_output](#structured_output) |
375
+ | Return structured JSON | Structured Output | [structured_output](#structured_output) |
355
376
  | Validate or filter content | Guardrails | [Guardrails](GUARDRAILS.md) |
356
377
  | Measure output quality | Evals | [Evals](EVALS.md) |
357
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|
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`:
@@ -465,10 +465,8 @@ module Riffer::Agent::Run
465
465
  tripwire = response.tripwire
466
466
  return unless tripwire
467
467
 
468
- class_name = tripwire.guardrail.name
469
- if class_name
470
- span.set_attribute("riffer.tripwire.guardrail", Riffer::Helpers::ClassNameConverter.convert(class_name))
471
- end
468
+ identifier = Riffer::Helpers::Identifier.for(tripwire.guardrail)
469
+ span.set_attribute("riffer.tripwire.guardrail", identifier) unless identifier.empty?
472
470
  span.set_attribute("riffer.tripwire.reason", tripwire.reason)
473
471
  span.set_attribute("riffer.tripwire.phase", tripwire.phase.to_s)
474
472
  end
data/lib/riffer/agent.rb CHANGED
@@ -15,6 +15,8 @@ require "json"
15
15
  # agent.generate('Hello!')
16
16
  #
17
17
  class Riffer::Agent
18
+ extend Riffer::Registrable
19
+
18
20
  # @rbs self.@config: Riffer::Agent::Config?
19
21
 
20
22
  INTERRUPT_MAX_STEPS = :max_steps #: Symbol
@@ -31,7 +33,7 @@ class Riffer::Agent
31
33
  #--
32
34
  #: (?String?) -> String
33
35
  def self.identifier(value = nil)
34
- value.nil? ? (config.identifier || Riffer::Helpers::ClassNameConverter.convert(name)) : (config.identifier = value)
36
+ value.nil? ? (config.identifier || Riffer::Helpers::Identifier.for(self)) : (config.identifier = value)
35
37
  end
36
38
 
37
39
  # Gets or sets the model string (e.g., "openai/gpt-4o").
@@ -144,22 +146,6 @@ class Riffer::Agent
144
146
  config.skills_config
145
147
  end
146
148
 
147
- # Finds an agent class by identifier.
148
- #
149
- #--
150
- #: (String) -> singleton(Riffer::Agent)?
151
- def self.find(identifier)
152
- all.find { |agent_class| agent_class.identifier == identifier.to_s }
153
- end
154
-
155
- # Returns all agent subclasses.
156
- #
157
- #--
158
- #: () -> Array[singleton(Riffer::Agent)]
159
- def self.all
160
- subclasses #: Array[singleton(Riffer::Agent)]
161
- end
162
-
163
149
  # Generates a response using a new agent instance.
164
150
  #--
165
151
  #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
@@ -36,7 +36,7 @@ class Riffer::Guardrail
36
36
  #--
37
37
  #: () -> String
38
38
  def name
39
- Riffer::Helpers::ClassNameConverter.convert(self.class.name)
39
+ Riffer::Helpers::Identifier.for(self.class)
40
40
  end
41
41
 
42
42
  protected
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Helper module for deriving snake_case identifiers from class names.
5
+ module Riffer::Helpers::Identifier
6
+ extend self
7
+
8
+ # Derives a snake_case identifier from a class name string.
9
+ #
10
+ #--
11
+ #: (String?) -> String
12
+ def derive(class_name)
13
+ class_name.
14
+ to_s.
15
+ gsub("::", "/").
16
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
17
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
18
+ downcase
19
+ end
20
+
21
+ # Derives and memoizes the identifier for a class or module. Anonymous
22
+ # classes return "" without caching, so a class named later still derives its
23
+ # real identifier — a guard that must travel with the cache, so callers never
24
+ # memoize their own.
25
+ #
26
+ #--
27
+ #: (Module) -> String
28
+ def for(klass)
29
+ cached = klass.instance_variable_get(:@derived_identifier) #: String?
30
+ return cached if cached
31
+
32
+ # Tool classes shadow Module#name with the identifier DSL, so the real
33
+ # class-path name must come from Module's own implementation.
34
+ real_name = Module.instance_method(:name).bind_call(klass) #: String?
35
+ return "" if real_name.nil?
36
+
37
+ derived = derive(real_name)
38
+ klass.instance_variable_set(:@derived_identifier, derived)
39
+ derived
40
+ end
41
+ end
@@ -292,6 +292,10 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
292
292
  #--
293
293
  #: (untyped, state: Hash[Symbol, untyped], yielder: Riffer::Providers::_EventSink) -> void
294
294
  def handle_input_json_event(event, state:, yielder:)
295
+ # server_tool_use (web_search) input streams through the raw-event path
296
+ # (handle_raw_content_block_delta) and must not surface as tool-call deltas.
297
+ return if state[:web_search_json]
298
+
295
299
  state[:tool_call] = { id: nil, name: nil, arguments: +"" } if state[:tool_call].nil?
296
300
  state[:tool_call][:arguments] << event.partial_json
297
301
  yielder << Riffer::StreamEvents::ToolCallDelta.new(
@@ -11,6 +11,7 @@ class Riffer::Providers::Base
11
11
  # @rbs @current_tools: Array[singleton(Riffer::Tool)]
12
12
  # @rbs @current_model: String?
13
13
  # @rbs @client: untyped
14
+ # @rbs self.@semconv_provider_name: String?
14
15
 
15
16
  WIRE_SEPARATOR = "__" #: String
16
17
 
@@ -30,9 +31,11 @@ class Riffer::Providers::Base
30
31
  #: () -> String
31
32
  def self.semconv_provider_name
32
33
  class_name = name
34
+ # Anonymous classes stay uncached: a class assigned to a constant
35
+ # later must pick up its real name, not a frozen "unknown".
33
36
  return "unknown" unless class_name
34
37
 
35
- Riffer::Helpers::ClassNameConverter.convert(class_name.split("::").last.to_s)
38
+ @semconv_provider_name ||= Riffer::Helpers::Identifier.derive(class_name.split("::").last)
36
39
  end
37
40
 
38
41
  # Generates text using the provider.
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Registry of a class's named direct subclasses, keyed by identifier. Extend it
5
+ # onto a base class to look up subclasses in constant time via +find+ and +all+.
6
+ #
7
+ # class Riffer::Tool
8
+ # extend Riffer::Registrable
9
+ # end
10
+ #
11
+ # Riffer::Tool.find("weather_tool") # => WeatherTool
12
+ #
13
+ # @rbs module-self Class
14
+ module Riffer::Registrable
15
+ # @rbs @identifier_registry: Hash[String, Class]?
16
+
17
+ # Finds a registered subclass by identifier, or +nil+ when none matches.
18
+ # Only *named direct* subclasses are registered: grandchildren are not
19
+ # visible to a grandparent's +find+ (call +find+ on their direct parent
20
+ # instead), anonymous classes are never registered, and duplicate identifiers
21
+ # raise Riffer::DuplicateIdentifierError at first lookup.
22
+ #
23
+ #--
24
+ #: (String | Symbol) -> Class?
25
+ def find(identifier)
26
+ identifier_registry[identifier.to_s]
27
+ end
28
+
29
+ # Returns all registered subclasses. Only *named direct* subclasses are
30
+ # registered: grandchildren are not included (call +all+ on their direct
31
+ # parent instead), anonymous classes are never registered, and duplicate
32
+ # identifiers raise Riffer::DuplicateIdentifierError at first lookup.
33
+ #
34
+ #--
35
+ #: () -> Array[Class]
36
+ def all
37
+ identifier_registry.values
38
+ end
39
+
40
+ private
41
+
42
+ # Ruby invokes +inherited+ with +self+ bound to the direct superclass — the
43
+ # only registry the new subclass joins — so busting self's memo is exactly
44
+ # sufficient.
45
+ #--
46
+ #: (Class) -> void
47
+ def inherited(subclass)
48
+ super
49
+ @identifier_registry = nil
50
+ end
51
+
52
+ #--
53
+ #: () -> Hash[String, Class]
54
+ def identifier_registry
55
+ @identifier_registry ||= build_identifier_registry
56
+ end
57
+
58
+ #--
59
+ #: () -> Hash[String, Class]
60
+ def build_identifier_registry
61
+ registry = {} #: Hash[String, Class]
62
+ subclasses.each_with_object(registry) do |subclass, acc|
63
+ # Anonymous classes are skipped even with an explicit identifier — the
64
+ # MCP factory and serializer shells synthesize short-lived anonymous
65
+ # classes whose registration would flake with GC timing.
66
+ next if Riffer::Helpers::Identifier.for(subclass).empty?
67
+
68
+ candidate = subclass #: untyped
69
+ key = candidate.identifier.to_s
70
+ next if key.strip.empty?
71
+
72
+ existing = acc[key]
73
+ if existing
74
+ raise Riffer::DuplicateIdentifierError,
75
+ "Duplicate identifier #{key.inspect} for #{existing} and #{subclass}"
76
+ end
77
+
78
+ acc[key] = subclass
79
+ end.freeze
80
+ end
81
+ end
data/lib/riffer/tool.rb CHANGED
@@ -21,6 +21,7 @@ require "timeout"
21
21
  #
22
22
  class Riffer::Tool
23
23
  extend Riffer::Tools::Toolable
24
+ extend Riffer::Registrable
24
25
 
25
26
  kind :tool
26
27
 
@@ -15,6 +15,7 @@
15
15
  # end
16
16
  # end
17
17
  #
18
+ # @rbs module-self Module
18
19
  module Riffer::Tools::Toolable
19
20
  # @rbs self.@extenders: Array[Module]?
20
21
  # @rbs @description: String?
@@ -57,9 +58,7 @@ module Riffer::Tools::Toolable
57
58
  #--
58
59
  #: (?String?) -> String
59
60
  def identifier(value = nil)
60
- if value.nil?
61
- return @identifier || Riffer::Helpers::ClassNameConverter.convert(Module.instance_method(:name).bind_call(self))
62
- end
61
+ return @identifier || Riffer::Helpers::Identifier.for(self) if value.nil?
63
62
 
64
63
  @identifier = value.to_s
65
64
  end
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.41.0" #: String
5
+ VERSION = "0.42.0" #: String
6
6
  end
data/lib/riffer.rb CHANGED
@@ -32,6 +32,9 @@ module Riffer
32
32
  # Raised when a tool encounters an expected execution error.
33
33
  class ToolExecutionError < Error; end
34
34
 
35
+ # Raised when two registered subclasses share the same identifier.
36
+ class DuplicateIdentifierError < Error; end
37
+
35
38
  # Returns the Riffer configuration.
36
39
  #
37
40
  #--
@@ -11,6 +11,8 @@
11
11
  # agent = MyAgent.new
12
12
  # agent.generate('Hello!')
13
13
  class Riffer::Agent
14
+ extend Riffer::Registrable
15
+
14
16
  self.@config: Riffer::Agent::Config?
15
17
 
16
18
  INTERRUPT_MAX_STEPS: Symbol
@@ -104,18 +106,6 @@ class Riffer::Agent
104
106
  # : () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
105
107
  def self.skills: () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
106
108
 
107
- # Finds an agent class by identifier.
108
- #
109
- # --
110
- # : (String) -> singleton(Riffer::Agent)?
111
- def self.find: (String) -> singleton(Riffer::Agent)?
112
-
113
- # Returns all agent subclasses.
114
- #
115
- # --
116
- # : () -> Array[singleton(Riffer::Agent)]
117
- def self.all: () -> Array[singleton(Riffer::Agent)]
118
-
119
109
  # Generates a response using a new agent instance.
120
110
  # --
121
111
  # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
@@ -0,0 +1,19 @@
1
+ # Generated from lib/riffer/helpers/identifier.rb with RBS::Inline
2
+
3
+ # Helper module for deriving snake_case identifiers from class names.
4
+ module Riffer::Helpers::Identifier
5
+ # Derives a snake_case identifier from a class name string.
6
+ #
7
+ # --
8
+ # : (String?) -> String
9
+ def derive: (String?) -> String
10
+
11
+ # Derives and memoizes the identifier for a class or module. Anonymous
12
+ # classes return "" without caching, so a class named later still derives its
13
+ # real identifier — a guard that must travel with the cache, so callers never
14
+ # memoize their own.
15
+ #
16
+ # --
17
+ # : (Module) -> String
18
+ def for: (Module) -> String
19
+ end
@@ -5,6 +5,8 @@
5
5
  # +extract_token_usage+, +extract_content+, +extract_tool_calls+) and the base
6
6
  # class orchestrates them.
7
7
  class Riffer::Providers::Base
8
+ self.@semconv_provider_name: String?
9
+
8
10
  @client: untyped
9
11
 
10
12
  @current_model: String?