riffer 0.16.1 → 0.17.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/architecture.md +11 -0
- data/.agents/code-style.md +7 -0
- data/.agents/rbs-inline.md +2 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +24 -0
- data/docs/03_AGENTS.md +62 -14
- data/docs/04_TOOLS.md +125 -4
- data/docs/06_STREAM_EVENTS.md +1 -1
- data/docs/07_CONFIGURATION.md +20 -0
- data/docs/08_EVALS.md +5 -5
- data/docs_providers/05_MOCK_PROVIDER.md +1 -1
- data/lib/riffer/agent.rb +136 -83
- data/lib/riffer/config.rb +19 -0
- data/lib/riffer/evals/evaluator_runner.rb +9 -9
- data/lib/riffer/evals/judge.rb +3 -3
- data/lib/riffer/guardrails/runner.rb +1 -1
- data/lib/riffer/messages/converter.rb +16 -14
- data/lib/riffer/params.rb +4 -17
- data/lib/riffer/providers/anthropic.rb +3 -3
- data/lib/riffer/runner/sequential.rb +13 -0
- data/lib/riffer/runner/threaded.rb +60 -0
- data/lib/riffer/runner.rb +24 -0
- data/lib/riffer/structured_output.rb +2 -2
- data/lib/riffer/tool_runtime/inline.rb +13 -0
- data/lib/riffer/tool_runtime/threaded.rb +19 -0
- data/lib/riffer/tool_runtime.rb +106 -0
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +3 -0
- data/sig/generated/riffer/agent.rbs +55 -18
- data/sig/generated/riffer/config.rbs +14 -0
- data/sig/generated/riffer/evals/evaluator_runner.rbs +6 -6
- data/sig/generated/riffer/guardrails/runner.rbs +1 -1
- data/sig/generated/riffer/messages/converter.rbs +6 -6
- data/sig/generated/riffer/params.rbs +0 -3
- data/sig/generated/riffer/runner/sequential.rbs +9 -0
- data/sig/generated/riffer/runner/threaded.rbs +24 -0
- data/sig/generated/riffer/runner.rbs +20 -0
- data/sig/generated/riffer/tool_runtime/inline.rbs +9 -0
- data/sig/generated/riffer/tool_runtime/threaded.rbs +15 -0
- data/sig/generated/riffer/tool_runtime.rbs +64 -0
- data/sig/generated/riffer.rbs +4 -0
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 849df927faaf4614ad3a4833aafa5323731d3c1f4a7a8687db35ee5d341eb334
|
|
4
|
+
data.tar.gz: 1d992d83487a673fef8714d6f72811eb94fdb5e646e33612eb18ac915257b832
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1967690dfbe21181d4670117de7563936c05dd7d7303597378584acba35a0543de292fb9e4f72d9ed71789d4b73dc17023ce1d1c0bba27434dea93646dccc4d
|
|
7
|
+
data.tar.gz: 71b4c1568a2b4f5f420f3546283f5c56bc979b8234734049a8df714363faf132f36bc892dabc3e62702c59c43e74ad7e697fb702c772f2bfdec6f56950c0a1fb
|
data/.agents/architecture.md
CHANGED
|
@@ -16,6 +16,17 @@ agent = EchoAgent.new
|
|
|
16
16
|
puts agent.generate('Hello world')
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`instructions` also accepts a Proc for dynamic instructions resolved at generate time. The Proc receives the `context` hash:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class PersonalAgent < Riffer::Agent
|
|
23
|
+
model 'openai/gpt-5-mini'
|
|
24
|
+
instructions ->(context) { "You are assisting #{context[:name]}" }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
PersonalAgent.generate('Hello!', context: { name: 'Jane' })
|
|
28
|
+
```
|
|
29
|
+
|
|
19
30
|
### Providers (`lib/riffer/providers/`)
|
|
20
31
|
|
|
21
32
|
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:
|
data/.agents/code-style.md
CHANGED
|
@@ -30,6 +30,13 @@ end
|
|
|
30
30
|
- Explain **why** something is done, not **what** is being done
|
|
31
31
|
- Comments should add value beyond what the code already expresses
|
|
32
32
|
|
|
33
|
+
## Hash Key Convention
|
|
34
|
+
|
|
35
|
+
- Use **symbol keys** for all internal hashes
|
|
36
|
+
- Use `JSON.parse(str, symbolize_names: true)` at parse boundaries — never `JSON.parse` followed by `transform_keys(&:to_sym)`
|
|
37
|
+
- String keys are only used at serialization boundaries (JSON Schema output, external API payloads)
|
|
38
|
+
- Do not write dual-access patterns like `hash[:key] || hash["key"]` — normalize to symbol keys at the boundary instead
|
|
39
|
+
|
|
33
40
|
## Module Structure
|
|
34
41
|
|
|
35
42
|
```ruby
|
data/.agents/rbs-inline.md
CHANGED
|
@@ -64,8 +64,8 @@ def evaluate(input:, output:)
|
|
|
64
64
|
def evaluate(input:, output:, context: nil)
|
|
65
65
|
|
|
66
66
|
# Positional + keyword parameters
|
|
67
|
-
#: (String, ?
|
|
68
|
-
def generate(prompt,
|
|
67
|
+
#: (String, ?context: Hash[Symbol, untyped]?) -> String
|
|
68
|
+
def generate(prompt, context: nil)
|
|
69
69
|
|
|
70
70
|
# Splat/double-splat
|
|
71
71
|
#: (**untyped) -> void
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ 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.17.0](https://github.com/janeapp/riffer/compare/riffer/v0.16.1...riffer/v0.17.0) (2026-03-06)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* rename `tool_context` to `context` ([#159](https://github.com/janeapp/riffer/issues/159))
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* add experimental ToolRuntime abstraction for tool execution ([#156](https://github.com/janeapp/riffer/issues/156)) ([0ca7563](https://github.com/janeapp/riffer/commit/0ca7563df9f0a555e5fa6a1f3065d5f072abbf7e))
|
|
18
|
+
* add interrupt! public method for clean loop interrupts ([#155](https://github.com/janeapp/riffer/issues/155)) ([a4cc877](https://github.com/janeapp/riffer/commit/a4cc8778b754e3932748454446eebd71795ad5e1))
|
|
19
|
+
* add support for dynamic instructions ([#158](https://github.com/janeapp/riffer/issues/158)) ([408e09c](https://github.com/janeapp/riffer/commit/408e09c585142caca173a232ec20dde012553dc0))
|
|
20
|
+
* auto-derive step offset on resume for max_steps enforcement ([#154](https://github.com/janeapp/riffer/issues/154)) ([fb97dbe](https://github.com/janeapp/riffer/commit/fb97dbec4a0edf5ea1e46bf44b93170663db04ef))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* resolve edge cases in generate/resume and streaming methods ([#162](https://github.com/janeapp/riffer/issues/162)) ([f74d373](https://github.com/janeapp/riffer/commit/f74d373fb3cb8bb2d6c4617dd29ba3b30a3a8177))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Code Refactoring
|
|
29
|
+
|
|
30
|
+
* rename `tool_context` to `context` ([#159](https://github.com/janeapp/riffer/issues/159)) ([5be7214](https://github.com/janeapp/riffer/commit/5be7214934866dfa24062b248c59324178f9956a))
|
|
31
|
+
|
|
8
32
|
## [0.16.1](https://github.com/janeapp/riffer/compare/riffer/v0.16.0...riffer/v0.16.1) (2026-03-03)
|
|
9
33
|
|
|
10
34
|
|
data/docs/03_AGENTS.md
CHANGED
|
@@ -37,12 +37,12 @@ class MyAgent < Riffer::Agent
|
|
|
37
37
|
end
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
When the lambda accepts a parameter, it receives the `
|
|
40
|
+
When the lambda accepts a parameter, it receives the `context`:
|
|
41
41
|
|
|
42
42
|
```ruby
|
|
43
43
|
class MyAgent < Riffer::Agent
|
|
44
|
-
model ->(
|
|
45
|
-
|
|
44
|
+
model ->(context) {
|
|
45
|
+
context&.dig(:premium) ? "anthropic/claude-sonnet-4-20250514" : "anthropic/claude-haiku-4-5-20251001"
|
|
46
46
|
}
|
|
47
47
|
end
|
|
48
48
|
```
|
|
@@ -60,6 +60,28 @@ class MyAgent < Riffer::Agent
|
|
|
60
60
|
end
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
Instructions can also be resolved dynamically with a lambda:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class MyAgent < Riffer::Agent
|
|
67
|
+
model 'openai/gpt-4o'
|
|
68
|
+
instructions -> { "Today is #{Date.today}. You are a helpful assistant." }
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
When the lambda accepts a parameter, it receives the `context`:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class MyAgent < Riffer::Agent
|
|
76
|
+
model 'openai/gpt-4o'
|
|
77
|
+
instructions ->(ctx) { "You are assisting #{ctx[:name]}" }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
MyAgent.generate('Hello!', context: { name: 'Jane' })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The lambda is re-evaluated on each `generate` or `stream` call, so instructions can change between calls based on runtime context.
|
|
84
|
+
|
|
63
85
|
### identifier
|
|
64
86
|
|
|
65
87
|
Sets a custom identifier (defaults to snake_case class name):
|
|
@@ -219,6 +241,22 @@ Using both `of:` and a block raises `Riffer::ArgumentError`. Using `of:` with a
|
|
|
219
241
|
|
|
220
242
|
Structured output is not compatible with streaming — calling `stream` on an agent with structured output configured raises `Riffer::ArgumentError`.
|
|
221
243
|
|
|
244
|
+
### tool_runtime (Experimental)
|
|
245
|
+
|
|
246
|
+
> **Warning:** This feature is experimental and may be removed or changed without warning in a future release.
|
|
247
|
+
|
|
248
|
+
Configures how tool calls are executed. Defaults to sequential (inline) execution:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
class MyAgent < Riffer::Agent
|
|
252
|
+
model 'openai/gpt-4o'
|
|
253
|
+
uses_tools [WeatherTool, SearchTool]
|
|
254
|
+
tool_runtime Riffer::ToolRuntime::Threaded
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Accepts a `Riffer::ToolRuntime` subclass, a `Riffer::ToolRuntime` instance, or a `Proc`. Inherited by subclasses. When unset, falls back to `Riffer.config.tool_runtime`. See [Tools — Tool Runtime](04_TOOLS.md#tool-runtime-experimental) for details.
|
|
259
|
+
|
|
222
260
|
### guardrail
|
|
223
261
|
|
|
224
262
|
Registers guardrails for pre/post processing of messages. Pass the guardrail class and any options:
|
|
@@ -266,8 +304,8 @@ response = MyAgent.generate([
|
|
|
266
304
|
{role: 'user', content: 'How are you?'}
|
|
267
305
|
])
|
|
268
306
|
|
|
269
|
-
# With
|
|
270
|
-
response = MyAgent.generate('Look up my orders',
|
|
307
|
+
# With context
|
|
308
|
+
response = MyAgent.generate('Look up my orders', context: {user_id: 123})
|
|
271
309
|
|
|
272
310
|
# With files (string prompt + files shorthand)
|
|
273
311
|
response = MyAgent.generate('What is in this image?', files: [
|
|
@@ -352,17 +390,17 @@ Works with both `generate` and `stream`. Only emits agent-generated messages (As
|
|
|
352
390
|
|
|
353
391
|
#### Interrupting the Agent Loop
|
|
354
392
|
|
|
355
|
-
Callbacks can interrupt the agent loop
|
|
393
|
+
Callbacks can interrupt the agent loop. This is useful for human-in-the-loop approval, cost limits, or content filtering.
|
|
356
394
|
|
|
357
|
-
Use `throw :riffer_interrupt` to stop the loop. The response will have `interrupted?` set to `true` and contain the accumulated content up to the point of interruption.
|
|
395
|
+
Use `agent.interrupt!` (or the lower-level `throw :riffer_interrupt`) to stop the loop. The response will have `interrupted?` set to `true` and contain the accumulated content up to the point of interruption.
|
|
358
396
|
|
|
359
|
-
An optional reason can be passed
|
|
397
|
+
An optional reason can be passed to `interrupt!`. It is available via `interrupt_reason` on the response (generate) or `reason` on the `Interrupt` event (stream):
|
|
360
398
|
|
|
361
399
|
```ruby
|
|
362
400
|
agent = MyAgent.new
|
|
363
401
|
agent.on_message do |msg|
|
|
364
402
|
if msg.is_a?(Riffer::Messages::Tool)
|
|
365
|
-
|
|
403
|
+
agent.interrupt!("needs human approval")
|
|
366
404
|
end
|
|
367
405
|
end
|
|
368
406
|
|
|
@@ -410,7 +448,7 @@ For cross-process resume (e.g., after a process restart or async approval), pass
|
|
|
410
448
|
# Persist messages during generation (e.g., via on_message callback)
|
|
411
449
|
# Later, in a new process:
|
|
412
450
|
agent = MyAgent.new
|
|
413
|
-
response = agent.resume(messages: persisted_messages,
|
|
451
|
+
response = agent.resume(messages: persisted_messages, context: {user_id: 123})
|
|
414
452
|
|
|
415
453
|
# Or resume in streaming mode:
|
|
416
454
|
agent.resume_stream(messages: persisted_messages).each do |event|
|
|
@@ -429,7 +467,7 @@ Continues an agent loop synchronously. Returns a `Riffer::Agent::Response` objec
|
|
|
429
467
|
response = agent.resume
|
|
430
468
|
|
|
431
469
|
# Cross-process resume from persisted messages
|
|
432
|
-
response = agent.resume(messages: persisted_messages,
|
|
470
|
+
response = agent.resume(messages: persisted_messages, context: {user_id: 123})
|
|
433
471
|
```
|
|
434
472
|
|
|
435
473
|
### resume_stream
|
|
@@ -449,6 +487,16 @@ agent.resume_stream(messages: persisted_messages).each do |event|
|
|
|
449
487
|
end
|
|
450
488
|
```
|
|
451
489
|
|
|
490
|
+
### interrupt!
|
|
491
|
+
|
|
492
|
+
Interrupts the agent loop from an `on_message` callback. Equivalent to `throw :riffer_interrupt, reason`:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
agent.on_message do |msg|
|
|
496
|
+
agent.interrupt!(:needs_approval) if requires_approval?(msg)
|
|
497
|
+
end
|
|
498
|
+
```
|
|
499
|
+
|
|
452
500
|
### token_usage
|
|
453
501
|
|
|
454
502
|
Access cumulative token usage across all LLM calls:
|
|
@@ -532,7 +580,7 @@ end
|
|
|
532
580
|
When an agent receives a response with tool calls:
|
|
533
581
|
|
|
534
582
|
1. Agent detects `tool_calls` in the assistant message
|
|
535
|
-
2.
|
|
583
|
+
2. The configured tool runtime executes the tool calls (sequentially by default, or concurrently with `Riffer::ToolRuntime::Threaded`):
|
|
536
584
|
- Finds the matching tool class
|
|
537
585
|
- Validates arguments against the tool's parameter schema
|
|
538
586
|
- Calls the tool's `call` method with `context` and arguments
|
|
@@ -576,7 +624,7 @@ response.tripwire.reason # => "Content policy violation"
|
|
|
576
624
|
|
|
577
625
|
### Callback Interrupt (imperative, external)
|
|
578
626
|
|
|
579
|
-
Callbacks registered with `on_message` can call `throw :riffer_interrupt` to pause the loop at any point — after receiving an assistant message, after a tool result, etc. The caller controls exactly when and why to interrupt.
|
|
627
|
+
Callbacks registered with `on_message` can call `agent.interrupt!` (or `throw :riffer_interrupt`) to pause the loop at any point — after receiving an assistant message, after a tool result, etc. The caller controls exactly when and why to interrupt.
|
|
580
628
|
|
|
581
629
|
- **When to use:** Flow control that depends on runtime decisions — human-in-the-loop approval, budget tracking, conditional pausing.
|
|
582
630
|
- **Response:** `response.interrupted?` returns `true`, `response.interrupt_reason` contains the optional reason.
|
|
@@ -586,7 +634,7 @@ Callbacks registered with `on_message` can call `throw :riffer_interrupt` to pau
|
|
|
586
634
|
```ruby
|
|
587
635
|
agent = MyAgent.new
|
|
588
636
|
agent.on_message do |msg|
|
|
589
|
-
|
|
637
|
+
agent.interrupt!("approval needed") if requires_approval?(msg)
|
|
590
638
|
end
|
|
591
639
|
|
|
592
640
|
response = agent.generate('Do something risky')
|
data/docs/04_TOOLS.md
CHANGED
|
@@ -145,7 +145,7 @@ Every tool must implement the `call` method and return a `Riffer::Tools::Respons
|
|
|
145
145
|
|
|
146
146
|
```ruby
|
|
147
147
|
def call(context:, **kwargs)
|
|
148
|
-
# context - The
|
|
148
|
+
# context - The context passed to agent.generate()
|
|
149
149
|
# kwargs - Validated parameters
|
|
150
150
|
#
|
|
151
151
|
# Must return a Riffer::Tools::Response
|
|
@@ -154,7 +154,7 @@ end
|
|
|
154
154
|
|
|
155
155
|
### Accessing Context
|
|
156
156
|
|
|
157
|
-
The `context` argument receives whatever was passed to `
|
|
157
|
+
The `context` argument receives whatever was passed as `context:` to `generate`:
|
|
158
158
|
|
|
159
159
|
```ruby
|
|
160
160
|
class UserOrdersTool < Riffer::Tool
|
|
@@ -172,7 +172,7 @@ class UserOrdersTool < Riffer::Tool
|
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
# Usage
|
|
175
|
-
agent.generate("Show my orders",
|
|
175
|
+
agent.generate("Show my orders", context: {user_id: 123})
|
|
176
176
|
```
|
|
177
177
|
|
|
178
178
|
## Response Objects
|
|
@@ -368,6 +368,127 @@ rescue => e
|
|
|
368
368
|
end
|
|
369
369
|
```
|
|
370
370
|
|
|
371
|
-
Unhandled exceptions are caught by Riffer and converted to error responses with type `:execution_error`.
|
|
371
|
+
Unhandled `RuntimeError` exceptions are caught by Riffer and converted to error responses with type `:execution_error`. For expected execution errors, raise `Riffer::ToolExecutionError` — these are also caught and returned to the LLM. Programming bugs (`NoMethodError`, `NameError`, `TypeError`, etc.) propagate to the caller. It's recommended to handle expected errors explicitly for better error messages.
|
|
372
372
|
|
|
373
373
|
The LLM receives the error message and can decide how to respond (retry, apologize, ask for different input, etc.).
|
|
374
|
+
|
|
375
|
+
## Tool Runtime (Experimental)
|
|
376
|
+
|
|
377
|
+
> **Warning:** This feature is experimental and may be removed or changed without warning in a future release.
|
|
378
|
+
|
|
379
|
+
By default, tool calls are executed sequentially in the current thread using `Riffer::ToolRuntime::Inline`. You can change how tool calls are executed by configuring a different tool runtime.
|
|
380
|
+
|
|
381
|
+
### Built-in Runtimes
|
|
382
|
+
|
|
383
|
+
| Runtime | Description |
|
|
384
|
+
|---------|-------------|
|
|
385
|
+
| `Riffer::ToolRuntime::Inline` | Executes tool calls sequentially (default) |
|
|
386
|
+
| `Riffer::ToolRuntime::Threaded` | Executes tool calls concurrently using threads |
|
|
387
|
+
|
|
388
|
+
### Per-Agent Configuration
|
|
389
|
+
|
|
390
|
+
Use the `tool_runtime` class method on your agent:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
class MyAgent < Riffer::Agent
|
|
394
|
+
model 'openai/gpt-4o'
|
|
395
|
+
uses_tools [WeatherTool, SearchTool]
|
|
396
|
+
tool_runtime Riffer::ToolRuntime::Threaded
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Accepted values:
|
|
401
|
+
|
|
402
|
+
- A `Riffer::ToolRuntime` subclass — instantiated automatically (e.g., `Riffer::ToolRuntime::Inline`, `Riffer::ToolRuntime::Threaded`)
|
|
403
|
+
- A `Riffer::ToolRuntime` instance — for custom runtimes with specific options
|
|
404
|
+
- A `Proc` — evaluated at runtime (see below)
|
|
405
|
+
|
|
406
|
+
### Dynamic Resolution
|
|
407
|
+
|
|
408
|
+
Use a lambda for context-aware runtime selection:
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
class MyAgent < Riffer::Agent
|
|
412
|
+
model 'openai/gpt-4o'
|
|
413
|
+
uses_tools [WeatherTool, SearchTool]
|
|
414
|
+
|
|
415
|
+
tool_runtime ->(context) {
|
|
416
|
+
context&.dig(:parallel) ? Riffer::ToolRuntime::Threaded.new : Riffer::ToolRuntime::Inline.new
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
agent.generate("Do work", context: {parallel: true})
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
When the lambda accepts a parameter, it receives the `context`. Zero-arity lambdas are also supported.
|
|
424
|
+
|
|
425
|
+
### Global Configuration
|
|
426
|
+
|
|
427
|
+
Set a default tool runtime for all agents:
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
Riffer.configure do |config|
|
|
431
|
+
config.tool_runtime = Riffer::ToolRuntime::Threaded
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Per-agent configuration overrides the global default.
|
|
436
|
+
|
|
437
|
+
### Threaded Runtime Considerations
|
|
438
|
+
|
|
439
|
+
When using `Riffer::ToolRuntime::Threaded`, each tool call runs in its own thread. The `around_tool_call` hook also runs inside that thread. Be mindful of thread-local state — for example, `ActiveRecord::Base.connection`, `RequestStore`, or any `Thread.current[]` values may not be available or may behave differently across threads. Ensure your tools and hooks are thread-safe.
|
|
440
|
+
|
|
441
|
+
### Threaded Runtime Options
|
|
442
|
+
|
|
443
|
+
The threaded runtime accepts a `max_concurrency` option (default: 5):
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
class MyAgent < Riffer::Agent
|
|
447
|
+
model 'openai/gpt-4o'
|
|
448
|
+
uses_tools [WeatherTool, SearchTool]
|
|
449
|
+
tool_runtime Riffer::ToolRuntime::Threaded.new(max_concurrency: 3)
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Custom Runtimes
|
|
454
|
+
|
|
455
|
+
Create a custom runtime by subclassing `Riffer::ToolRuntime` and overriding the private `dispatch_tool_call` method:
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
class HttpToolRuntime < Riffer::ToolRuntime
|
|
459
|
+
private
|
|
460
|
+
|
|
461
|
+
def dispatch_tool_call(tool_call, tools:, context:)
|
|
462
|
+
# Dispatch tool execution to an external service
|
|
463
|
+
response = HttpClient.post("/tools/execute", {
|
|
464
|
+
name: tool_call.name,
|
|
465
|
+
arguments: tool_call.arguments
|
|
466
|
+
})
|
|
467
|
+
Riffer::Tools::Response.text(response.body)
|
|
468
|
+
rescue Riffer::ToolExecutionError => e
|
|
469
|
+
Riffer::Tools::Response.error(e.message, type: :execution_error)
|
|
470
|
+
rescue RuntimeError => e
|
|
471
|
+
Riffer::Tools::Response.error("Error executing tool: #{e.message}", type: :execution_error)
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Around-Call Hook
|
|
477
|
+
|
|
478
|
+
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:
|
|
479
|
+
|
|
480
|
+
```ruby
|
|
481
|
+
class InstrumentedRuntime < Riffer::ToolRuntime::Inline
|
|
482
|
+
private
|
|
483
|
+
|
|
484
|
+
def around_tool_call(tool_call, context:)
|
|
485
|
+
start = Time.now
|
|
486
|
+
result = yield
|
|
487
|
+
duration = Time.now - start
|
|
488
|
+
Rails.logger.info("Tool #{tool_call.name} took #{duration}s")
|
|
489
|
+
result
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Subclasses inherit the hook and can override it further.
|
data/docs/06_STREAM_EVENTS.md
CHANGED
|
@@ -190,7 +190,7 @@ See [Guardrails](09_GUARDRAILS.md) for more information.
|
|
|
190
190
|
|
|
191
191
|
Emitted when the agent loop is interrupted. This can happen in two ways:
|
|
192
192
|
|
|
193
|
-
- An `on_message` callback calls `throw :riffer_interrupt` (reason is a String or `nil`).
|
|
193
|
+
- An `on_message` callback calls `agent.interrupt!` or `throw :riffer_interrupt` (reason is a String or `nil`).
|
|
194
194
|
- The `max_steps` limit is reached (reason is the Symbol `:max_steps`).
|
|
195
195
|
|
|
196
196
|
This is the streaming equivalent of `Response#interrupted?` in generate mode.
|
data/docs/07_CONFIGURATION.md
CHANGED
|
@@ -72,6 +72,26 @@ Riffer.configure do |config|
|
|
|
72
72
|
end
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
### Tool Runtime (Experimental)
|
|
76
|
+
|
|
77
|
+
> **Warning:** This feature is experimental and may be removed or changed without warning in a future release.
|
|
78
|
+
|
|
79
|
+
Configure the default tool runtime for all agents:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Riffer.configure do |config|
|
|
83
|
+
config.tool_runtime = Riffer::ToolRuntime::Threaded
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Value | Description |
|
|
88
|
+
|-------|-------------|
|
|
89
|
+
| `Riffer::ToolRuntime` subclass | Instantiated automatically (e.g., `Riffer::ToolRuntime::Inline`, `Riffer::ToolRuntime::Threaded`) |
|
|
90
|
+
| `Riffer::ToolRuntime` instance | Custom runtime with specific options |
|
|
91
|
+
| `Proc` | Dynamic resolution |
|
|
92
|
+
|
|
93
|
+
Per-agent configuration overrides this global default. See [Tools — Tool Runtime](04_TOOLS.md#tool-runtime-experimental) for details.
|
|
94
|
+
|
|
75
95
|
## Agent-Level Configuration
|
|
76
96
|
|
|
77
97
|
Override global configuration at the agent level:
|
data/docs/08_EVALS.md
CHANGED
|
@@ -81,23 +81,23 @@ result = Riffer::Evals::EvaluatorRunner.run(
|
|
|
81
81
|
)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
###
|
|
84
|
+
### Context
|
|
85
85
|
|
|
86
|
-
Pass `
|
|
86
|
+
Pass `context:` to provide context that agents use for dynamic model selection, tool resolution, or tool execution:
|
|
87
87
|
|
|
88
88
|
```ruby
|
|
89
89
|
result = Riffer::Evals::EvaluatorRunner.run(
|
|
90
90
|
agent: MyAgent,
|
|
91
91
|
scenarios: [
|
|
92
92
|
{ input: "What is Ruby?" },
|
|
93
|
-
{ input: "Premium question",
|
|
93
|
+
{ input: "Premium question", context: { premium: true } }
|
|
94
94
|
],
|
|
95
95
|
evaluators: [AnswerRelevancyEvaluator],
|
|
96
|
-
|
|
96
|
+
context: { premium: false }
|
|
97
97
|
)
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Per-scenario `
|
|
100
|
+
Per-scenario `context` overrides the top-level value. Scenarios without their own `context` inherit the top-level value.
|
|
101
101
|
|
|
102
102
|
### RunResult
|
|
103
103
|
|
|
@@ -121,7 +121,7 @@ class MyAgentTest < Minitest::Test
|
|
|
121
121
|
])
|
|
122
122
|
@provider.stub_response("Done.")
|
|
123
123
|
|
|
124
|
-
@agent.generate("Do something",
|
|
124
|
+
@agent.generate("Do something", context: {user_id: 123})
|
|
125
125
|
|
|
126
126
|
# Tool receives the context
|
|
127
127
|
end
|