riffer 0.41.0 → 0.43.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 +22 -0
- data/docs/AGENTS.md +24 -3
- data/docs/AGENT_LIFECYCLE.md +2 -2
- data/docs/AGENT_LOOP.md +6 -4
- data/docs/TOOLS.md +22 -2
- data/docs/TOOL_ADVANCED.md +20 -7
- data/docs/TRACING.md +5 -4
- 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/messages/tool.rb +2 -1
- 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 +25 -9
- data/lib/riffer/tools/response.rb +10 -5
- data/lib/riffer/tools/runtime.rb +20 -11
- data/lib/riffer/tools/toolable.rb +2 -3
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +6 -1
- data/sig/generated/riffer/agent.rbs +2 -12
- data/sig/generated/riffer/helpers/identifier.rbs +19 -0
- data/sig/generated/riffer/messages/tool.rbs +2 -1
- data/sig/generated/riffer/providers/base.rbs +2 -0
- data/sig/generated/riffer/registrable.rbs +51 -0
- data/sig/generated/riffer/tool.rbs +6 -5
- data/sig/generated/riffer/tools/response.rbs +8 -4
- data/sig/generated/riffer/tools/runtime.rbs +5 -4
- data/sig/generated/riffer/tools/toolable.rbs +3 -1
- data/sig/generated/riffer.rbs +7 -1
- 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: bed1103d2399d8532635b4041d4a24b8264f160b58a3110436ed67a14380fd56
|
|
4
|
+
data.tar.gz: acdaa65506e0c4887d388663be686bc02ccf668c26db281148efcec9f5772d6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0733aa1731a7206195862e6bfd59936b80cc3219fa913a6afce577a8de9d8f4510ca59316a6bb27c6a778706b6d231ce61cd74ebd9d63f32d2a695453b2bd5ae
|
|
7
|
+
data.tar.gz: da09bd24389e52f79c7cbe3dd977c5ada7bc9f0a923f2d09d2331245b4ca1fd079f9f688da0ab8b36eac6b1a260bd627831dc0c344ae93422f5a006c929a11e3
|
|
@@ -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,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.43.0](https://github.com/janeapp/riffer/compare/riffer/v0.42.0...riffer/v0.43.0) (2026-08-27)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* call_with_validation no longer raises Riffer::ValidationError / Riffer::TimeoutError / Riffer::Error — it always returns a Riffer::Tools::Response. Riffer::TimeoutError has been removed; delete any `rescue Riffer::TimeoutError` and check `response.error_type` instead. Custom runtimes overriding dispatch_tool_call no longer inherit rescues from the base class.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* tools are the error boundary — call_with_validation never raises ([#412](https://github.com/janeapp/riffer/issues/412)) ([b6db9b6](https://github.com/janeapp/riffer/commit/b6db9b6ab2885d170cbba52c7a571c63b83d892e))
|
|
18
|
+
|
|
19
|
+
## [0.42.0](https://github.com/janeapp/riffer/compare/riffer/v0.41.0...riffer/v0.42.0) (2026-08-25)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### ⚠ BREAKING CHANGES
|
|
23
|
+
|
|
24
|
+
* 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).
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* one identifier registry pattern for tools and agents ([#406](https://github.com/janeapp/riffer/issues/406)) ([72fa7d6](https://github.com/janeapp/riffer/commit/72fa7d6377f903e4a88f9f5ec0e27e51b46dc293))
|
|
29
|
+
|
|
8
30
|
## [0.41.0](https://github.com/janeapp/riffer/compare/riffer/v0.40.0...riffer/v0.41.0) (2026-08-14)
|
|
9
31
|
|
|
10
32
|
|
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/AGENT_LOOP.md
CHANGED
|
@@ -15,13 +15,15 @@ When an agent receives a response with tool calls:
|
|
|
15
15
|
|
|
16
16
|
## Error Handling
|
|
17
17
|
|
|
18
|
-
Tool execution errors
|
|
18
|
+
Tool execution errors never abort the loop — each is captured and sent back to the LLM as a tool result:
|
|
19
19
|
|
|
20
20
|
- `unknown_tool` - Tool not found in registered tools
|
|
21
|
-
- `validation_error` - Arguments failed validation
|
|
22
|
-
- `
|
|
21
|
+
- `validation_error` - Arguments failed validation or were malformed JSON
|
|
22
|
+
- `timeout_error` - Tool exceeded its configured timeout
|
|
23
|
+
- `execution_error` - Tool returned an error or raised `Riffer::ToolExecutionError`
|
|
24
|
+
- `unhandled_error` - Tool raised an unanticipated exception
|
|
23
25
|
|
|
24
|
-
The LLM can use this information to retry or respond appropriately.
|
|
26
|
+
The LLM can use this information to retry or respond appropriately. See [Error Handling](TOOL_ADVANCED.md#error-handling) for details.
|
|
25
27
|
|
|
26
28
|
## Ways the Agent Loop Can Stop
|
|
27
29
|
|
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`:
|
|
@@ -252,7 +269,7 @@ error("Service unavailable", type: :service_error)
|
|
|
252
269
|
error("Rate limit exceeded", type: :rate_limit)
|
|
253
270
|
```
|
|
254
271
|
|
|
255
|
-
If no type is specified, it defaults to `:execution_error`.
|
|
272
|
+
If no type is specified, it defaults to `:execution_error`. Riffer reserves `:unhandled_error` for unrescued exceptions — don't set it yourself.
|
|
256
273
|
|
|
257
274
|
### Using Riffer::Tools::Response Directly
|
|
258
275
|
|
|
@@ -280,4 +297,7 @@ error_response.success? # => false
|
|
|
280
297
|
error_response.error? # => true
|
|
281
298
|
error_response.error_message # => "failed"
|
|
282
299
|
error_response.error_type # => :not_found
|
|
300
|
+
error_response.exception # => nil
|
|
283
301
|
```
|
|
302
|
+
|
|
303
|
+
`exception` holds the rescued exception on the `:unhandled_error` responses Riffer builds. It never appears in serialized output, so it is not visible to the LLM.
|
data/docs/TOOL_ADVANCED.md
CHANGED
|
@@ -16,7 +16,7 @@ class SlowExternalApiTool < Riffer::Tool
|
|
|
16
16
|
end
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
When a tool times out, the
|
|
19
|
+
When a tool times out, the LLM receives an error response with type `:timeout_error` and can respond appropriately (e.g., suggest retrying or using a different approach). The timeout raises `Riffer::TimeoutError` inside `call`, so a tool can rescue it to release resources before it propagates.
|
|
20
20
|
|
|
21
21
|
## Validation
|
|
22
22
|
|
|
@@ -26,7 +26,7 @@ Arguments are automatically validated before `call` is invoked:
|
|
|
26
26
|
- Types must match the schema
|
|
27
27
|
- Enum values must be in the allowed list
|
|
28
28
|
|
|
29
|
-
Validation errors are captured and sent back to the LLM as tool results with error type `:validation_error
|
|
29
|
+
Validation errors are captured and sent back to the LLM as tool results with error type `:validation_error`, as is malformed or non-object JSON in the provider's tool-call arguments.
|
|
30
30
|
|
|
31
31
|
## JSON Schema Generation
|
|
32
32
|
|
|
@@ -95,7 +95,20 @@ rescue => e
|
|
|
95
95
|
end
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
A tool never raises into the agent loop — every `StandardError` raised during a tool call becomes an error response:
|
|
99
|
+
|
|
100
|
+
| Failure | Error type | Response content |
|
|
101
|
+
| ---------------------------- | ------------------- | ---------------------------------------------------- |
|
|
102
|
+
| Invalid arguments | `:validation_error` | the validation message |
|
|
103
|
+
| Timeout | `:timeout_error` | `Tool execution timed out after N seconds` |
|
|
104
|
+
| `Riffer::ToolExecutionError` | `:execution_error` | the exception message |
|
|
105
|
+
| Any other `StandardError` | `:unhandled_error` | `Error executing tool: <ExceptionClass>: <message>` |
|
|
106
|
+
|
|
107
|
+
An `:unhandled_error` response also carries the rescued exception on `response.exception` — never serialized, so it stays out of the message history — and its `execute_tool` span records the exception with an `ERROR` status (see [Tracing](TRACING.md)).
|
|
108
|
+
|
|
109
|
+
`NotImplementedError` is not rescued: an unimplemented `#call` raises out of the run.
|
|
110
|
+
|
|
111
|
+
For expected failures, return `error(...)` or raise `Riffer::ToolExecutionError` — both give the LLM a clean message rather than an `:unhandled_error`.
|
|
99
112
|
|
|
100
113
|
The LLM receives the error message and can decide how to respond (retry, apologize, ask for different input, etc.).
|
|
101
114
|
|
|
@@ -222,14 +235,14 @@ class HttpToolRuntime < Riffer::Tools::Runtime
|
|
|
222
235
|
arguments: tool_call.arguments
|
|
223
236
|
})
|
|
224
237
|
Riffer::Tools::Response.text(response.body)
|
|
225
|
-
rescue
|
|
226
|
-
Riffer::Tools::Response.error(e.message, type: :execution_error)
|
|
227
|
-
rescue RuntimeError => e
|
|
228
|
-
Riffer::Tools::Response.error("Error executing tool: #{e.message}", type: :execution_error)
|
|
238
|
+
rescue HttpClient::Error => e
|
|
239
|
+
Riffer::Tools::Response.error("Tool service unavailable: #{e.message}", type: :execution_error)
|
|
229
240
|
end
|
|
230
241
|
end
|
|
231
242
|
```
|
|
232
243
|
|
|
244
|
+
Anything that escapes `dispatch_tool_call` propagates out of the run — rescue whatever your transport can raise and return an error response. The base class handles only an unknown tool name and malformed argument JSON.
|
|
245
|
+
|
|
233
246
|
### Around-Call Hook
|
|
234
247
|
|
|
235
248
|
Each tool call is wrapped by the `around_tool_call` method, which yields by default. Override it in a subclass to add instrumentation, logging, or other cross-cutting concerns:
|
data/docs/TRACING.md
CHANGED
|
@@ -168,12 +168,13 @@ Usage on this span is the run total, aggregated across every step. See [Token us
|
|
|
168
168
|
| `gen_ai.tool.call.arguments` | string | When `capture_messages` is on (see [capture](#message-content-capture)) |
|
|
169
169
|
| `gen_ai.tool.call.result` | string | When `capture_messages` is on |
|
|
170
170
|
|
|
171
|
-
A tool failure comes in
|
|
171
|
+
A tool failure comes in three shapes, distinguished by span status:
|
|
172
172
|
|
|
173
|
-
- **Handled error** — the tool
|
|
174
|
-
- **Unhandled
|
|
173
|
+
- **Handled error** — the tool call produced a deliberate error response, from the tool itself or from the runtime (an unknown tool, malformed arguments). `error.type` carries the category and the **span status stays unset** (the run continues). The framework's categories are `unknown_tool`, `validation_error`, `timeout_error`, and `execution_error`; a custom tool may set its own via `Riffer::Tools::Response.error(type:)`.
|
|
174
|
+
- **Unhandled error** — the tool raised an unanticipated `StandardError`. `error.type` is `unhandled_error`, the **span status is `ERROR`**, and the exception is recorded on the span. The run continues — the LLM receives the error response.
|
|
175
|
+
- **Host code raising** around the tool call (an `around_tool_call` hook, a tracing callback) propagates out of the run. `error.type` is the exception class name and the span status is `ERROR`.
|
|
175
176
|
|
|
176
|
-
This status convention is the same on `chat` and `invoke_agent`: an unhandled exception
|
|
177
|
+
This status convention is the same on `chat` and `invoke_agent`: an unhandled exception marks the span `ERROR` with the exception recorded; a handled outcome leaves the status unset.
|
|
177
178
|
|
|
178
179
|
## `execute_guardrail {name}` — the guardrail span
|
|
179
180
|
|
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
|
data/lib/riffer/messages/tool.rb
CHANGED
|
@@ -12,7 +12,8 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
|
|
|
12
12
|
# The error message if the tool execution failed.
|
|
13
13
|
attr_reader :error #: String?
|
|
14
14
|
|
|
15
|
-
# The type of error (:unknown_tool, :validation_error, :execution_error,
|
|
15
|
+
# The type of error (:unknown_tool, :validation_error, :execution_error,
|
|
16
|
+
# :timeout_error, :unhandled_error).
|
|
16
17
|
attr_reader :error_type #: Symbol?
|
|
17
18
|
|
|
18
19
|
#--
|
|
@@ -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.
|