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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Generated from lib/riffer/registrable.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Registry of a class's named direct subclasses, keyed by identifier. Extend it
|
|
4
|
+
# onto a base class to look up subclasses in constant time via +find+ and +all+.
|
|
5
|
+
#
|
|
6
|
+
# class Riffer::Tool
|
|
7
|
+
# extend Riffer::Registrable
|
|
8
|
+
# end
|
|
9
|
+
#
|
|
10
|
+
# Riffer::Tool.find("weather_tool") # => WeatherTool
|
|
11
|
+
#
|
|
12
|
+
# @rbs module-self Class
|
|
13
|
+
module Riffer::Registrable : Class
|
|
14
|
+
@identifier_registry: Hash[String, Class]?
|
|
15
|
+
|
|
16
|
+
# Finds a registered subclass by identifier, or +nil+ when none matches.
|
|
17
|
+
# Only *named direct* subclasses are registered: grandchildren are not
|
|
18
|
+
# visible to a grandparent's +find+ (call +find+ on their direct parent
|
|
19
|
+
# instead), anonymous classes are never registered, and duplicate identifiers
|
|
20
|
+
# raise Riffer::DuplicateIdentifierError at first lookup.
|
|
21
|
+
#
|
|
22
|
+
# --
|
|
23
|
+
# : (String | Symbol) -> Class?
|
|
24
|
+
def find: (String | Symbol) -> Class?
|
|
25
|
+
|
|
26
|
+
# Returns all registered subclasses. Only *named direct* subclasses are
|
|
27
|
+
# registered: grandchildren are not included (call +all+ on their direct
|
|
28
|
+
# parent instead), anonymous classes are never registered, and duplicate
|
|
29
|
+
# identifiers raise Riffer::DuplicateIdentifierError at first lookup.
|
|
30
|
+
#
|
|
31
|
+
# --
|
|
32
|
+
# : () -> Array[Class]
|
|
33
|
+
def all: () -> Array[Class]
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Ruby invokes +inherited+ with +self+ bound to the direct superclass — the
|
|
38
|
+
# only registry the new subclass joins — so busting self's memo is exactly
|
|
39
|
+
# sufficient.
|
|
40
|
+
# --
|
|
41
|
+
# : (Class) -> void
|
|
42
|
+
def inherited: (Class) -> void
|
|
43
|
+
|
|
44
|
+
# --
|
|
45
|
+
# : () -> Hash[String, Class]
|
|
46
|
+
def identifier_registry: () -> Hash[String, Class]
|
|
47
|
+
|
|
48
|
+
# --
|
|
49
|
+
# : () -> Hash[String, Class]
|
|
50
|
+
def build_identifier_registry: () -> Hash[String, Class]
|
|
51
|
+
end
|
data/sig/generated/riffer.rbs
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `Riffer::Agent` extends `Riffer::Registrable`, whose generic signatures return
|
|
2
|
+
# `Class`. Narrow them here so callers get the agent singleton type back.
|
|
3
|
+
class Riffer::Agent
|
|
4
|
+
def self.find: (String | Symbol) -> singleton(Riffer::Agent)?
|
|
5
|
+
|
|
6
|
+
def self.all: () -> Array[singleton(Riffer::Agent)]
|
|
7
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `Riffer::Tool` extends `Riffer::Registrable`, whose generic signatures return
|
|
2
|
+
# `Class`. Narrow them here so callers get the tool singleton type back.
|
|
3
|
+
class Riffer::Tool
|
|
4
|
+
def self.find: (String | Symbol) -> singleton(Riffer::Tool)?
|
|
5
|
+
|
|
6
|
+
def self.all: () -> Array[singleton(Riffer::Tool)]
|
|
7
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riffer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.42.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -41,19 +41,15 @@ extra_rdoc_files:
|
|
|
41
41
|
- LICENSE.txt
|
|
42
42
|
- README.md
|
|
43
43
|
files:
|
|
44
|
-
- ".agents/architecture.md"
|
|
45
|
-
- ".agents/code-style.md"
|
|
46
|
-
- ".agents/providers.md"
|
|
47
|
-
- ".agents/rbs-inline.md"
|
|
48
|
-
- ".agents/testing.md"
|
|
49
44
|
- ".bundle/config"
|
|
45
|
+
- ".claude/CLAUDE.md"
|
|
46
|
+
- ".claude/rules/comments.md"
|
|
47
|
+
- ".claude/rules/rbs-inline.md"
|
|
50
48
|
- ".release-please-config.json"
|
|
51
49
|
- ".release-please-manifest.json"
|
|
52
50
|
- ".rubocop.yml"
|
|
53
51
|
- ".ruby-version"
|
|
54
|
-
- AGENTS.md
|
|
55
52
|
- CHANGELOG.md
|
|
56
|
-
- CLAUDE.md
|
|
57
53
|
- CODE_OF_CONDUCT.md
|
|
58
54
|
- Guardfile
|
|
59
55
|
- LICENSE.txt
|
|
@@ -123,8 +119,8 @@ files:
|
|
|
123
119
|
- lib/riffer/helpers.rb
|
|
124
120
|
- lib/riffer/helpers/boolean.rb
|
|
125
121
|
- lib/riffer/helpers/call_or_value.rb
|
|
126
|
-
- lib/riffer/helpers/class_name_converter.rb
|
|
127
122
|
- lib/riffer/helpers/dependencies.rb
|
|
123
|
+
- lib/riffer/helpers/identifier.rb
|
|
128
124
|
- lib/riffer/mcp.rb
|
|
129
125
|
- lib/riffer/mcp/authenticated_tool.rb
|
|
130
126
|
- lib/riffer/mcp/client.rb
|
|
@@ -157,6 +153,7 @@ files:
|
|
|
157
153
|
- lib/riffer/providers/open_router.rb
|
|
158
154
|
- lib/riffer/providers/repository.rb
|
|
159
155
|
- lib/riffer/providers/token_usage.rb
|
|
156
|
+
- lib/riffer/registrable.rb
|
|
160
157
|
- lib/riffer/runner.rb
|
|
161
158
|
- lib/riffer/runner/fibers.rb
|
|
162
159
|
- lib/riffer/runner/sequential.rb
|
|
@@ -242,8 +239,8 @@ files:
|
|
|
242
239
|
- sig/generated/riffer/helpers.rbs
|
|
243
240
|
- sig/generated/riffer/helpers/boolean.rbs
|
|
244
241
|
- sig/generated/riffer/helpers/call_or_value.rbs
|
|
245
|
-
- sig/generated/riffer/helpers/class_name_converter.rbs
|
|
246
242
|
- sig/generated/riffer/helpers/dependencies.rbs
|
|
243
|
+
- sig/generated/riffer/helpers/identifier.rbs
|
|
247
244
|
- sig/generated/riffer/mcp.rbs
|
|
248
245
|
- sig/generated/riffer/mcp/authenticated_tool.rbs
|
|
249
246
|
- sig/generated/riffer/mcp/client.rbs
|
|
@@ -276,6 +273,7 @@ files:
|
|
|
276
273
|
- sig/generated/riffer/providers/open_router.rbs
|
|
277
274
|
- sig/generated/riffer/providers/repository.rbs
|
|
278
275
|
- sig/generated/riffer/providers/token_usage.rbs
|
|
276
|
+
- sig/generated/riffer/registrable.rbs
|
|
279
277
|
- sig/generated/riffer/runner.rbs
|
|
280
278
|
- sig/generated/riffer/runner/fibers.rbs
|
|
281
279
|
- sig/generated/riffer/runner/sequential.rbs
|
|
@@ -322,20 +320,22 @@ files:
|
|
|
322
320
|
- sig/generated/riffer/version.rbs
|
|
323
321
|
- sig/manifest.yaml
|
|
324
322
|
- sig/manual/riffer.rbs
|
|
323
|
+
- sig/manual/riffer/agent.rbs
|
|
325
324
|
- sig/manual/riffer/agent/run.rbs
|
|
326
325
|
- sig/manual/riffer/agent/serializer.rbs
|
|
327
326
|
- sig/manual/riffer/agent/session/repair.rbs
|
|
328
327
|
- sig/manual/riffer/evals/evaluator_runner.rbs
|
|
329
328
|
- sig/manual/riffer/helpers/boolean.rbs
|
|
330
329
|
- sig/manual/riffer/helpers/call_or_value.rbs
|
|
331
|
-
- sig/manual/riffer/helpers/class_name_converter.rbs
|
|
332
330
|
- sig/manual/riffer/helpers/dependencies.rbs
|
|
331
|
+
- sig/manual/riffer/helpers/identifier.rbs
|
|
333
332
|
- sig/manual/riffer/mcp.rbs
|
|
334
333
|
- sig/manual/riffer/mcp/authenticated_tool.rbs
|
|
335
334
|
- sig/manual/riffer/mcp/registry.rbs
|
|
336
335
|
- sig/manual/riffer/mcp/tool_factory.rbs
|
|
337
336
|
- sig/manual/riffer/providers.rbs
|
|
338
337
|
- sig/manual/riffer/providers/repository.rbs
|
|
338
|
+
- sig/manual/riffer/tool.rbs
|
|
339
339
|
- sig/manual/riffer/tracing.rbs
|
|
340
340
|
- sig/manual/riffer/tracing/capture.rbs
|
|
341
341
|
- sig/manual/riffer/tracing/no_op.rbs
|
data/.agents/architecture.md
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
## Core Components
|
|
4
|
-
|
|
5
|
-
### Agent (`lib/riffer/agent.rb`)
|
|
6
|
-
|
|
7
|
-
Base class for AI agents. Subclass and use DSL methods `model`, `instructions`, `structured_output`, and `skills` to configure. Orchestrates message flow, LLM calls, tool execution, structured output parsing, and skill activation via a generate/stream loop.
|
|
8
|
-
|
|
9
|
-
Class-level DSL settings live on a per-class `Riffer::Agent::Config` object accessible via `MyAgent.config`. DSL methods read and mutate this Config in place. Instances read from `@config`, which defaults to `self.class.config` but can be replaced via `Agent.new(config: ...)`.
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class EchoAgent < Riffer::Agent
|
|
13
|
-
model 'openai/gpt-5-mini' # provider/model
|
|
14
|
-
instructions 'You are an assistant that repeats what the user says.'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
agent = EchoAgent.new
|
|
18
|
-
puts agent.generate('Hello world')
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
`instructions` also accepts a Proc for dynamic instructions resolved at generate time. The Proc receives the `context` hash:
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
class PersonalAgent < Riffer::Agent
|
|
25
|
-
model 'openai/gpt-5-mini'
|
|
26
|
-
instructions ->(context) { "You are assisting #{context[:name]}" }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
PersonalAgent.generate('Hello!', context: { name: 'Jane' })
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Providers (`lib/riffer/providers/`)
|
|
33
|
-
|
|
34
|
-
Adapters for LLM APIs. The base class uses a template-method pattern — `generate_text` and `stream_text` orchestrate the flow, delegating to five hook methods each provider implements:
|
|
35
|
-
|
|
36
|
-
- `build_request_params(messages, model, options)` — convert messages, tools, and options into SDK params
|
|
37
|
-
- `execute_generate(params)` — call the SDK and return the raw response
|
|
38
|
-
- `execute_stream(params, yielder)` — call the streaming SDK, mapping events to the yielder
|
|
39
|
-
- `extract_token_usage(response)` — pull token counts from the SDK response
|
|
40
|
-
- `extract_content(response)` — extract text content from the SDK response
|
|
41
|
-
- `extract_tool_calls(response)` — extract tool calls from the SDK response
|
|
42
|
-
|
|
43
|
-
Providers are registered in `Riffer::Providers::Repository::REPO` with identifiers (e.g., `openai`, `amazon_bedrock`).
|
|
44
|
-
|
|
45
|
-
Each provider declares a preferred skill adapter via `self.skills_adapter(model = nil)`. Default is Markdown; Anthropic returns XML; Amazon Bedrock returns XML when the model identifier matches an Anthropic model (e.g. `anthropic.claude-…` or `us.anthropic.claude-…`); Mock returns XML when the model name contains `claude`. The agent passes the resolved model identifier so proxy providers (Bedrock, Mock) can pick the right adapter without per-agent overrides.
|
|
46
|
-
|
|
47
|
-
### Skills (`lib/riffer/skills/`)
|
|
48
|
-
|
|
49
|
-
Support for the [Agent Skills spec](https://agentskills.io/). Skills are packaged as directories containing `SKILL.md` files with YAML frontmatter. The framework discovers skills through a pluggable backend, injects metadata into the system prompt, and provides a tool (`skill_activate`) for the LLM to load full skill instructions on demand.
|
|
50
|
-
|
|
51
|
-
- `Config` - DSL configuration object (`backend`, `adapter`, `activate`)
|
|
52
|
-
- `Backend` - base class interface (`list_skills`, `read_skill`)
|
|
53
|
-
- `FilesystemBackend` - built-in filesystem scanner
|
|
54
|
-
- `Frontmatter` - parsed YAML frontmatter value object with `.parse(raw)` class method
|
|
55
|
-
- `Context` - coordinates discovery, activation, caching, and prompt rendering for a generation cycle
|
|
56
|
-
- `Adapter` - base class for skill adapters (`render_catalog`, `activate_tool`)
|
|
57
|
-
- `MarkdownAdapter` - default Markdown skill adapter
|
|
58
|
-
- `XmlAdapter` - XML skill adapter for Anthropic/Claude
|
|
59
|
-
- `ActivateTool` - default tool the LLM calls to activate a skill
|
|
60
|
-
|
|
61
|
-
### Messages (`lib/riffer/messages/`)
|
|
62
|
-
|
|
63
|
-
Typed message objects that extend `Riffer::Messages::Base`:
|
|
64
|
-
|
|
65
|
-
- `System` - system instructions
|
|
66
|
-
- `User` - user input (supports file attachments via `Riffer::Messages::FilePart`)
|
|
67
|
-
- `Assistant` - AI responses
|
|
68
|
-
- `Tool` - tool execution results
|
|
69
|
-
|
|
70
|
-
`Riffer::Messages::FilePart` represents file attachments (images and documents) that can be included with User messages. Supports file paths, URLs, and raw base64 data.
|
|
71
|
-
|
|
72
|
-
The `Converter` module handles hash-to-object conversion, including file hash-to-`FilePart` conversion.
|
|
73
|
-
|
|
74
|
-
### StreamEvents (`lib/riffer/stream_events/`)
|
|
75
|
-
|
|
76
|
-
Structured events for streaming responses:
|
|
77
|
-
|
|
78
|
-
- `TextDelta` - incremental text chunks
|
|
79
|
-
- `TextDone` - completion signals
|
|
80
|
-
- `ReasoningDelta` - reasoning process chunks
|
|
81
|
-
- `ReasoningDone` - reasoning completion
|
|
82
|
-
- `WebSearchStatus` - web search status updates
|
|
83
|
-
- `WebSearchDone` - web search completion with query and sources
|
|
84
|
-
- `Interrupt` - callback interrupted the agent loop
|
|
85
|
-
|
|
86
|
-
### Per-Call State Reset
|
|
87
|
-
|
|
88
|
-
Each call to `generate` or `stream` resets `context`, tools, tool runtime, model, skills state, and the interrupted flag via `prepare_run`. Only the message history and cumulative `token_usage` persist across calls. This means `context:` must be passed on every call.
|
|
89
|
-
|
|
90
|
-
### Stopping the Loop Early
|
|
91
|
-
|
|
92
|
-
Two mechanisms can stop the agent loop before the LLM finishes naturally:
|
|
93
|
-
|
|
94
|
-
**Guardrail tripwires** — declarative policy enforcement registered at class level. A `:before` guardrail can block the request before the LLM is called; an `:after` guardrail can block the response. Tripwires are not resumable — the caller must change the input and start over. `Response#blocked?` returns `true`.
|
|
95
|
-
|
|
96
|
-
**Callback interrupts** — imperative flow control via `on_message` callbacks. Use `throw :riffer_interrupt` to pause the loop at any point. `Response#interrupted?` returns `true`. In streaming, yields an `Interrupt` event.
|
|
97
|
-
|
|
98
|
-
### Resuming After an Interrupt
|
|
99
|
-
|
|
100
|
-
Two resume paths:
|
|
101
|
-
|
|
102
|
-
- **In-memory** — call `generate` or `stream` again with a string on the same agent instance. The message history is preserved and the new user message is appended.
|
|
103
|
-
- **Cross-process** — pass persisted messages as an array to a new agent instance. Array input uses messages as-is (no system message prepend). Passing an array to an agent that already has messages raises `Riffer::ArgumentError`.
|
|
104
|
-
|
|
105
|
-
```ruby
|
|
106
|
-
agent.generate('Continue') # in-memory resume
|
|
107
|
-
MyAgent.new.stream(persisted_messages) # cross-process resume
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
On resume, `execute_pending_tool_calls` detects tool calls from the last assistant message that lack corresponding tool result messages and executes them before entering the LLM loop. This handles the case where an interrupt fired mid-way through tool execution.
|
|
111
|
-
|
|
112
|
-
### Runner (`lib/riffer/runner.rb`)
|
|
113
|
-
|
|
114
|
-
Concurrency primitive for batch execution. Subclasses implement `#map(items, context: nil, &block)` to control how items are processed. The `context` keyword carries the agent's context hash, enabling runners that need it for job serialization or routing.
|
|
115
|
-
|
|
116
|
-
Built-in runners:
|
|
117
|
-
|
|
118
|
-
- `Sequential` — processes items in the current thread via `Array#map`
|
|
119
|
-
- `Threaded` — processes items concurrently using a thread pool with configurable `max_concurrency`
|
|
120
|
-
|
|
121
|
-
```ruby
|
|
122
|
-
runner = Riffer::Runner::Threaded.new(max_concurrency: 3)
|
|
123
|
-
runner.map(items, context: ctx) { |item| process(item) }
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Tools::Runtime (`lib/riffer/tools/runtime.rb`)
|
|
127
|
-
|
|
128
|
-
Composes with a Runner to execute tool calls. Provides `#execute` as the public entry point and `#around_tool_call` as a hook for instrumentation. Passes the agent context through to the runner.
|
|
129
|
-
|
|
130
|
-
Built-in runtimes:
|
|
131
|
-
|
|
132
|
-
- `Inline` — uses `Runner::Sequential` (default)
|
|
133
|
-
- `Threaded` — uses `Runner::Threaded`
|
|
134
|
-
|
|
135
|
-
Context flow: `Agent#execute_tool_calls` → `Tools::Runtime#execute(tool_calls, tools:, context:)` → `Runner#map(tool_calls, context:) { dispatch }` → `Tool#call(context:, **args)`
|
|
136
|
-
|
|
137
|
-
### MCP Integration (`lib/riffer/mcp/`)
|
|
138
|
-
|
|
139
|
-
Register third-party MCP servers globally; agents opt-in by tag via `use_mcp`. Tags are application-defined (manifests may list several; any overlap with `use_mcp` opts in—see `docs/MCP.md`).
|
|
140
|
-
|
|
141
|
-
```ruby
|
|
142
|
-
Riffer::Mcp.register(
|
|
143
|
-
name: "github",
|
|
144
|
-
tags: [:github],
|
|
145
|
-
endpoint: "https://mcp.github.com",
|
|
146
|
-
discovery_headers: -> { {"Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}"} }
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
# Optional: per-run tools/call headers (see docs/MCP.md)
|
|
150
|
-
Riffer.configure { |c| c.mcp.credentials = ->(manifest:, matched_tags:, context:) { ... } }
|
|
151
|
-
|
|
152
|
-
class ResearchAgent < Riffer::Agent
|
|
153
|
-
model "openai/gpt-5-mini"
|
|
154
|
-
use_mcp :github # picks up any :github-tagged registration
|
|
155
|
-
use_mcp :search, on_pending: :wait # per-call override
|
|
156
|
-
end
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Key types: `Manifest` (`discovery_headers`, optional `credentials_scope` hint), `Registry` (thread-safe store), `Registration` (spawns discovery thread → `ToolFactory`), `Client` (wraps `mcp` gem), `AuthenticatedTool` (wraps MCP tools when `credentials` proc is set).
|
|
160
|
-
|
|
161
|
-
**on_pending strategies** (global default `:ignore`): `:ignore` skips the server; `:wait` blocks until ready, re-raises failed discovery immediately, or times out; `:raise` re-raises failed discovery or `NotReadyError` while still pending.
|
|
162
|
-
|
|
163
|
-
## Key Patterns
|
|
164
|
-
|
|
165
|
-
- Model config accepts a `provider/model` string (e.g., `openai/gpt-5-mini`) or a Proc/lambda that returns one
|
|
166
|
-
- Configuration via `Riffer.configure { |c| c.openai.api_key = "..." }`
|
|
167
|
-
- Providers use `depends_on` helper for runtime dependency checking
|
|
168
|
-
- Zeitwerk for autoloading - file structure must match module/class names
|
|
169
|
-
|
|
170
|
-
## Project Structure
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
examples/
|
|
174
|
-
evaluators/ # Reference evaluator implementations (copy-paste)
|
|
175
|
-
guardrails/ # Reference guardrail implementations (copy-paste)
|
|
176
|
-
lib/
|
|
177
|
-
riffer.rb # Main entry point, uses Zeitwerk for autoloading
|
|
178
|
-
riffer/
|
|
179
|
-
version.rb # VERSION constant
|
|
180
|
-
config.rb # Configuration class
|
|
181
|
-
agent.rb # Agent class
|
|
182
|
-
agent/
|
|
183
|
-
config.rb # Per-class DSL configuration value object
|
|
184
|
-
session.rb # Conversation handle (message array + invariants)
|
|
185
|
-
session/
|
|
186
|
-
repair.rb # tool_use ↔ tool_result invariant repair
|
|
187
|
-
structured_output.rb # Structured output schema wrapper
|
|
188
|
-
structured_output/
|
|
189
|
-
result.rb # Parse/validation result object
|
|
190
|
-
messages.rb # Messages namespace/module
|
|
191
|
-
providers.rb # Providers namespace/module
|
|
192
|
-
params.rb # Parameter collection with DSL and validation
|
|
193
|
-
params/
|
|
194
|
-
param.rb # Single parameter definition (shared by tools and structured output)
|
|
195
|
-
boolean.rb # Boolean sentinel type
|
|
196
|
-
stream_events.rb # Stream events namespace/module
|
|
197
|
-
skills.rb # Skills namespace/module
|
|
198
|
-
skills/
|
|
199
|
-
config.rb # DSL configuration object
|
|
200
|
-
adapter.rb # Adapter base class (render_catalog, activate_tool)
|
|
201
|
-
markdown_adapter.rb # Default Markdown skill adapter
|
|
202
|
-
xml_adapter.rb # XML skill adapter for Anthropic/Claude
|
|
203
|
-
backend.rb # Backend base class (interface)
|
|
204
|
-
filesystem_backend.rb # Built-in filesystem backend
|
|
205
|
-
frontmatter.rb # Parsed YAML frontmatter value object with .parse
|
|
206
|
-
context.rb # Skills context for a generation cycle
|
|
207
|
-
activate_tool.rb # Default skill_activate tool
|
|
208
|
-
helpers/
|
|
209
|
-
class_name_converter.rb # Class name conversion utilities
|
|
210
|
-
dependencies.rb # Dependency management
|
|
211
|
-
messages/
|
|
212
|
-
base.rb # Base message class
|
|
213
|
-
assistant.rb # Assistant message
|
|
214
|
-
converter.rb # Message converter
|
|
215
|
-
system.rb # System message
|
|
216
|
-
user.rb # User message
|
|
217
|
-
tool.rb # Tool message
|
|
218
|
-
file_part.rb # File attachment (images and documents)
|
|
219
|
-
providers/
|
|
220
|
-
base.rb # Base provider class
|
|
221
|
-
open_ai.rb # OpenAI provider
|
|
222
|
-
amazon_bedrock.rb # Amazon Bedrock provider
|
|
223
|
-
anthropic.rb # Anthropic provider
|
|
224
|
-
repository.rb # Provider registry
|
|
225
|
-
mock.rb # Mock provider
|
|
226
|
-
stream_events/
|
|
227
|
-
base.rb # Base stream event
|
|
228
|
-
interrupt.rb # Interrupt event
|
|
229
|
-
text_delta.rb # Text delta event
|
|
230
|
-
text_done.rb # Text done event
|
|
231
|
-
reasoning_delta.rb # Reasoning delta event
|
|
232
|
-
reasoning_done.rb # Reasoning done event
|
|
233
|
-
web_search_status.rb # Web search status event
|
|
234
|
-
web_search_done.rb # Web search done event
|
|
235
|
-
mcp.rb # MCP public API + error classes
|
|
236
|
-
mcp/
|
|
237
|
-
manifest.rb # Server config value object
|
|
238
|
-
registry.rb # Thread-safe global store
|
|
239
|
-
registration.rb # Per-server state + discovery thread
|
|
240
|
-
client.rb # Thin mcp gem wrapper
|
|
241
|
-
authenticated_tool.rb # Wraps MCP tools when credentials proc is configured
|
|
242
|
-
tool_factory.rb # Generates Riffer::Tool subclasses from MCP tools
|
|
243
|
-
test/
|
|
244
|
-
test_helper.rb # Minitest configuration with VCR
|
|
245
|
-
riffer_test.rb # Main module tests
|
|
246
|
-
riffer/
|
|
247
|
-
[feature]_test.rb # Feature tests mirror lib/riffer/ structure
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## Configuration Example
|
|
251
|
-
|
|
252
|
-
```ruby
|
|
253
|
-
Riffer.configure do |config|
|
|
254
|
-
config.openai.api_key = ENV['OPENAI_API_KEY']
|
|
255
|
-
end
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## Streaming Example
|
|
259
|
-
|
|
260
|
-
```ruby
|
|
261
|
-
agent = EchoAgent.new
|
|
262
|
-
agent.stream('Tell me a story').each do |event|
|
|
263
|
-
print event.content
|
|
264
|
-
end
|
|
265
|
-
```
|
data/.agents/code-style.md
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Code Style
|
|
2
|
-
|
|
3
|
-
## Formatting
|
|
4
|
-
|
|
5
|
-
- Use RuboCop for linting and formatting
|
|
6
|
-
- Config lives in `.rubocop.yml`, with cops sorted alphabetically
|
|
7
|
-
- Run `bin/lint` to check, `bin/lint -a` to auto-fix (safe autocorrect), `bin/lint -A` for unsafe autocorrect
|
|
8
|
-
- Never silence violations with inline `rubocop:disable` comments — fix them, or when an exception is genuinely warranted add a narrowly scoped override in `.rubocop.yml` with a comment explaining why
|
|
9
|
-
|
|
10
|
-
## Required Header
|
|
11
|
-
|
|
12
|
-
All Ruby files in `lib/` must include:
|
|
13
|
-
|
|
14
|
-
```ruby
|
|
15
|
-
# frozen_string_literal: true
|
|
16
|
-
# rbs_inline: enabled
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Error Handling
|
|
20
|
-
|
|
21
|
-
Define custom errors as subclasses of `Riffer::Error`:
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
class MyCustomError < Riffer::Error
|
|
25
|
-
end
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Comments & Documentation
|
|
29
|
-
|
|
30
|
-
A comment exists to explain a **why** 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 RDoc descriptions on the public API. Types are not prose's job: parameters, return values, and attribute/constant types live in rbs-inline `#:` annotations (see [rbs-inline.md](rbs-inline.md)).
|
|
31
|
-
|
|
32
|
-
**What's a comment (in scope):** RDoc descriptions and inline `#` explanations. **Not comments (never touched):** rbs-inline `#:` annotations, magic comments (`frozen_string_literal`, `rbs_inline`), and RDoc directives (`:nodoc:`, `:nocov:`).
|
|
33
|
-
|
|
34
|
-
### Public surface
|
|
35
|
-
|
|
36
|
-
Everything public — classes, modules, constants, attributes, public methods, and `protected` subclass-contract methods (e.g. the `pass` / `transform` / `block` helpers a custom `Riffer::Guardrail` calls) — gets **at minimum a very brief description**:
|
|
37
|
-
|
|
38
|
-
- **One verb-first sentence, one line.** `Creates a new agent.`, `Serializes the definition to JSON.`
|
|
39
|
-
- An optional **second sentence is reserved strictly for a "why"** — a non-obvious constraint or rationale the code can't convey. Never a second sentence of "how".
|
|
40
|
-
- If a description needs more than one sentence to say _what_ it does, that's a smell the method does too much.
|
|
41
|
-
- **Exempt:** a constant whose name and value already carry the full meaning (`VERSION = "0.30.0"`, `PHASES = %i[before after]`) — describe a constant only when its name doesn't; an empty namespace module (a Zeitwerk placeholder with no usable members of its own, e.g. `module Riffer::Messages; end`) — its children are documented individually; and a constructor (`initialize`) — the class doc and RDoc's `::new` already cover plain construction, so describe it only when it carries a contract or non-obvious construction behavior (a raise, a dup guard).
|
|
42
|
-
|
|
43
|
-
Do not document parameters or return values in prose — the `#:` line is the single source of truth for types. Attributes and constants still carry a brief description on the line above their inline `#:`.
|
|
44
|
-
|
|
45
|
-
```ruby
|
|
46
|
-
# Serializes the agent definition to a transferable JSON payload.
|
|
47
|
-
#--
|
|
48
|
-
#: (Riffer::Agent) -> String
|
|
49
|
-
def serialize(agent)
|
|
50
|
-
|
|
51
|
-
# The agent's display name.
|
|
52
|
-
attr_reader :name #: String
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Private methods
|
|
56
|
-
|
|
57
|
-
A comment survives on a private method **only** if it explains a why a competent reader cannot recover from the code and names alone — a non-local constraint, an external-system quirk, a deliberate non-obvious tradeoff. A description of what it does, or a why that's evident from the code, gets cut.
|
|
58
|
-
|
|
59
|
-
### Inline comments
|
|
60
|
-
|
|
61
|
-
Same bar: kept only to explain the why of something genuinely ambiguous. `TODO` / `FIXME` / `HACK` markers are tracked work and stay; `NOTE` / `REVIEW` are subject to the why-rule.
|
|
62
|
-
|
|
63
|
-
### No history
|
|
64
|
-
|
|
65
|
-
A comment describes the present, never how the code got there. Change narration — "was X, now Y", "previously used Z" — has no place; the reader cares about what is, not what was. The one thing worth stating is a still-true constraint, and it belongs in the present tense ("the API returns null for empty results — guard"), never told as the story of the bug that revealed it.
|
|
66
|
-
|
|
67
|
-
### RDoc mechanics
|
|
68
|
-
|
|
69
|
-
**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.
|
|
70
|
-
|
|
71
|
-
**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. Reserved for public methods. 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 — but keep the runtime constraints a type can't express (enum value sets, coercion rules, validation failure).
|
|
72
|
-
|
|
73
|
-
```ruby
|
|
74
|
-
# Builds a param from a schema hash.
|
|
75
|
-
# Raises Riffer::ArgumentError if the schema is missing a +type+.
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**Examples.** Include an example only when a **consumer is likely to use the thing themselves** — a public entry point they construct, subclass, or call (an `Agent` subclass, `Riffer::Mcp.register`, the `params` DSL). Skip examples on framework-internal types even though they're technically public (value objects the framework constructs and hands back, internal engines). When included, keep them sparing — only when they teach something the signature can't — and write them as indented code blocks (2 extra spaces of indent). Usage walkthroughs belong in `docs/`.
|
|
79
|
-
|
|
80
|
-
**Inline code formatting.** Use `+word+` for single-word inline code; for multi-word expressions (spaces, colons, brackets) use `<tt>multi word expression</tt>`, e.g. `Equivalent to <tt>throw :riffer_interrupt, reason</tt>`.
|
|
81
|
-
|
|
82
|
-
**Internal APIs.** Mark with `:nodoc:` to exclude from generated documentation:
|
|
83
|
-
|
|
84
|
-
```ruby
|
|
85
|
-
def internal_method # :nodoc:
|
|
86
|
-
end
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Hash Key Convention
|
|
90
|
-
|
|
91
|
-
- Use **symbol keys** for all internal hashes
|
|
92
|
-
- Use `JSON.parse(str, symbolize_names: true)` at parse boundaries — never `JSON.parse` followed by `transform_keys(&:to_sym)`
|
|
93
|
-
- String keys are only used at serialization boundaries (JSON Schema output, external API payloads)
|
|
94
|
-
- Do not write dual-access patterns like `hash[:key] || hash["key"]` — normalize to symbol keys at the boundary instead
|
|
95
|
-
|
|
96
|
-
## Reserved Tool Identifiers
|
|
97
|
-
|
|
98
|
-
- Internal-use-only tools use plain descriptive names without a prefix (e.g. `mcp_search`, `mcp_call`, `evaluation`, `skill_activate`)
|
|
99
|
-
|
|
100
|
-
## Module Structure
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
# frozen_string_literal: true
|
|
104
|
-
|
|
105
|
-
module Riffer::Feature
|
|
106
|
-
class MyClass
|
|
107
|
-
# Implementation
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
```
|
data/.agents/providers.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Adding a New Provider
|
|
2
|
-
|
|
3
|
-
## Steps
|
|
4
|
-
|
|
5
|
-
1. Create `lib/riffer/providers/your_provider.rb` extending `Riffer::Providers::Base`
|
|
6
|
-
2. Implement the required hook methods (see [Custom Providers](../docs/providers/CUSTOM_PROVIDERS.md) for the full API)
|
|
7
|
-
3. Register in `Riffer::Providers::Repository::REPO`
|
|
8
|
-
4. Add provider config to `Riffer::Config` if needed — a `Struct` with credential members plus a `client` member
|
|
9
|
-
5. Create tests in `test/riffer/providers/your_provider_test.rb`
|
|
10
|
-
|
|
11
|
-
## Constructor and client contract
|
|
12
|
-
|
|
13
|
-
- Constructors take **no arguments** — define one only when the provider needs `depends_on`, and give it no parameters. Credentials live in config; never accept them as kwargs or hold them in ivars.
|
|
14
|
-
- Never hold a client ivar; call the private `client` method from `execute_generate`/`execute_stream`. Base resolves: `global_client` (a client instance, or a no-argument Proc resolved on every call) → memoized `build_client`.
|
|
15
|
-
- Providers hold no agent state — no context, no reference to the owning agent. Client selection is process-global by design, so a configured Proc takes no arguments.
|
|
16
|
-
- Implement `build_client` (build the SDK client by reading `Riffer.config.<provider>.<credential>` directly) and override `global_client` to return `Riffer.config.<provider>.client`.
|
|
17
|
-
- **Never pass an SDK an explicit nil credential.** Build the kwargs as a hash and `.compact` it, so an unset value stays _absent_: SDKs distinguish absent from nil to decide whether to read their own env vars, and an explicit nil suppresses that. Passing `base_url: nil` skips `OPENAI_BASE_URL` and pins requests to api.openai.com; passing `region: nil` makes the AWS SDK raise `MissingRegionError` even with `AWS_REGION` exported.
|
|
18
|
-
- **Exception — a provider borrowing another vendor's SDK** (`OpenRouter` and `AzureOpenAI` reuse `::OpenAI::Client`) must keep its credential and endpoint concrete, nil included. Compacting there would let the OpenAI SDK fall back to `OPENAI_API_KEY` / `OPENAI_BASE_URL` and send one vendor's credential to another's endpoint. Compact a key only when the SDK's env fallback for it names the same service the provider talks to.
|
|
19
|
-
|
|
20
|
-
## Architecture
|
|
21
|
-
|
|
22
|
-
The base class uses the **template method** pattern. The public methods `generate_text` and `stream_text` orchestrate the flow, delegating to hook methods that each provider implements:
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
generate_text
|
|
26
|
-
├─ build_request_params
|
|
27
|
-
├─ execute_generate
|
|
28
|
-
├─ extract_content
|
|
29
|
-
├─ extract_tool_calls
|
|
30
|
-
└─ extract_token_usage
|
|
31
|
-
|
|
32
|
-
stream_text
|
|
33
|
-
├─ build_request_params
|
|
34
|
-
└─ execute_stream
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Registration
|
|
38
|
-
|
|
39
|
-
Add to `Riffer::Providers::Repository::REPO`:
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
REPO = {
|
|
43
|
-
# ... existing providers
|
|
44
|
-
your_provider: -> { YourProvider }
|
|
45
|
-
}.freeze
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Dependencies
|
|
49
|
-
|
|
50
|
-
Use `depends_on` helper for runtime dependency checking if your provider requires external gems.
|
|
51
|
-
|
|
52
|
-
## Reference
|
|
53
|
-
|
|
54
|
-
For hook method signatures, structured output handling, file handling, and complete examples, see [Custom Providers](../docs/providers/CUSTOM_PROVIDERS.md).
|