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.
- checksums.yaml +4 -4
- data/{AGENTS.md → .claude/CLAUDE.md} +0 -8
- data/.claude/rules/comments.md +13 -0
- data/{.agents → .claude/rules}/rbs-inline.md +28 -104
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +11 -0
- data/docs/AGENTS.md +24 -3
- data/docs/AGENT_LIFECYCLE.md +2 -2
- data/docs/TOOLS.md +18 -1
- data/lib/riffer/agent/run.rb +2 -4
- data/lib/riffer/agent.rb +3 -17
- data/lib/riffer/guardrail.rb +1 -1
- data/lib/riffer/helpers/identifier.rb +41 -0
- data/lib/riffer/providers/anthropic.rb +4 -0
- data/lib/riffer/providers/base.rb +4 -1
- data/lib/riffer/registrable.rb +81 -0
- data/lib/riffer/tool.rb +1 -0
- data/lib/riffer/tools/toolable.rb +2 -3
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +3 -0
- data/sig/generated/riffer/agent.rbs +2 -12
- data/sig/generated/riffer/helpers/identifier.rbs +19 -0
- data/sig/generated/riffer/providers/base.rbs +2 -0
- data/sig/generated/riffer/registrable.rbs +51 -0
- data/sig/generated/riffer/tool.rbs +2 -0
- data/sig/generated/riffer/tools/toolable.rbs +3 -1
- data/sig/generated/riffer.rbs +4 -0
- data/sig/manual/riffer/agent.rbs +7 -0
- data/sig/manual/riffer/helpers/identifier.rbs +5 -0
- data/sig/manual/riffer/tool.rbs +7 -0
- metadata +11 -11
- data/.agents/architecture.md +0 -265
- data/.agents/code-style.md +0 -110
- data/.agents/providers.md +0 -54
- data/.agents/testing.md +0 -60
- data/CLAUDE.md +0 -1
- data/lib/riffer/helpers/class_name_converter.rb +0 -22
- data/sig/generated/riffer/helpers/class_name_converter.rbs +0 -12
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eabed1a1a6844787d92f31be0a72e210117005c19b4dd5e021a905121f83e8e1
|
|
4
|
+
data.tar.gz: 4cc6b6bde7e4b474d7ca6fb2672a3e115d4584b096f1de5d967a2b1a751f49ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
## Annotation Conventions
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
The **`#:`** prefix is used — standalone lines above methods (RBS method signatures) or inline on the same line (attributes, constants).
|
|
14
16
|
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
class Riffer::Agent::Session
|
|
21
|
+
# @rbs @callbacks: Array[^(Riffer::Messages::Base) -> void] # instance ivar
|
|
22
|
+
end
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
module Riffer
|
|
25
|
+
# @rbs self.@config: Riffer::Config? # class/module-level ivar
|
|
26
|
+
end
|
|
27
|
+
```
|
|
23
28
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
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) |
|
data/docs/AGENT_LIFECYCLE.md
CHANGED
|
@@ -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`
|
|
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`:
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -465,10 +465,8 @@ module Riffer::Agent::Run
|
|
|
465
465
|
tripwire = response.tripwire
|
|
466
466
|
return unless tripwire
|
|
467
467
|
|
|
468
|
-
|
|
469
|
-
|
|
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::
|
|
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
|
data/lib/riffer/guardrail.rb
CHANGED
|
@@ -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::
|
|
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
|
@@ -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
|
data/lib/riffer/version.rb
CHANGED
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
|