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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +11 -0
  3. data/.agents/code-style.md +7 -0
  4. data/.agents/rbs-inline.md +2 -2
  5. data/.release-please-manifest.json +1 -1
  6. data/CHANGELOG.md +24 -0
  7. data/docs/03_AGENTS.md +62 -14
  8. data/docs/04_TOOLS.md +125 -4
  9. data/docs/06_STREAM_EVENTS.md +1 -1
  10. data/docs/07_CONFIGURATION.md +20 -0
  11. data/docs/08_EVALS.md +5 -5
  12. data/docs_providers/05_MOCK_PROVIDER.md +1 -1
  13. data/lib/riffer/agent.rb +136 -83
  14. data/lib/riffer/config.rb +19 -0
  15. data/lib/riffer/evals/evaluator_runner.rb +9 -9
  16. data/lib/riffer/evals/judge.rb +3 -3
  17. data/lib/riffer/guardrails/runner.rb +1 -1
  18. data/lib/riffer/messages/converter.rb +16 -14
  19. data/lib/riffer/params.rb +4 -17
  20. data/lib/riffer/providers/anthropic.rb +3 -3
  21. data/lib/riffer/runner/sequential.rb +13 -0
  22. data/lib/riffer/runner/threaded.rb +60 -0
  23. data/lib/riffer/runner.rb +24 -0
  24. data/lib/riffer/structured_output.rb +2 -2
  25. data/lib/riffer/tool_runtime/inline.rb +13 -0
  26. data/lib/riffer/tool_runtime/threaded.rb +19 -0
  27. data/lib/riffer/tool_runtime.rb +106 -0
  28. data/lib/riffer/version.rb +1 -1
  29. data/lib/riffer.rb +3 -0
  30. data/sig/generated/riffer/agent.rbs +55 -18
  31. data/sig/generated/riffer/config.rbs +14 -0
  32. data/sig/generated/riffer/evals/evaluator_runner.rbs +6 -6
  33. data/sig/generated/riffer/guardrails/runner.rbs +1 -1
  34. data/sig/generated/riffer/messages/converter.rbs +6 -6
  35. data/sig/generated/riffer/params.rbs +0 -3
  36. data/sig/generated/riffer/runner/sequential.rbs +9 -0
  37. data/sig/generated/riffer/runner/threaded.rbs +24 -0
  38. data/sig/generated/riffer/runner.rbs +20 -0
  39. data/sig/generated/riffer/tool_runtime/inline.rbs +9 -0
  40. data/sig/generated/riffer/tool_runtime/threaded.rbs +15 -0
  41. data/sig/generated/riffer/tool_runtime.rbs +64 -0
  42. data/sig/generated/riffer.rbs +4 -0
  43. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd7f9f22c84f8d3680662d9cac4b242f0425395f510b36654c910b91448dc34f
4
- data.tar.gz: 1718dd3b5ca59e62f3287e7ecdfdff201de124bcd2230687a7e3d5ed554fdc95
3
+ metadata.gz: 849df927faaf4614ad3a4833aafa5323731d3c1f4a7a8687db35ee5d341eb334
4
+ data.tar.gz: 1d992d83487a673fef8714d6f72811eb94fdb5e646e33612eb18ac915257b832
5
5
  SHA512:
6
- metadata.gz: bda15f6aef23966f078cbae8be85dce02f177d9d51cc8fb9c08368dc113dbac8efe6ecbe356ae22a5eba043a98278622f1ddc10126eaada061b98eec849ea75c
7
- data.tar.gz: 02f951011443b6ce726292be3f19ceb870b1177c03a3eb78b876170aef4d667b583def11e0242acee130c6e87654c262a0f91df366daf537060be2f88aa33283
6
+ metadata.gz: f1967690dfbe21181d4670117de7563936c05dd7d7303597378584acba35a0543de292fb9e4f72d9ed71789d4b73dc17023ce1d1c0bba27434dea93646dccc4d
7
+ data.tar.gz: 71b4c1568a2b4f5f420f3546283f5c56bc979b8234734049a8df714363faf132f36bc892dabc3e62702c59c43e74ad7e697fb702c772f2bfdec6f56950c0a1fb
@@ -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:
@@ -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
@@ -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, ?tool_context: Hash[Symbol, untyped]?) -> String
68
- def generate(prompt, tool_context: nil)
67
+ #: (String, ?context: Hash[Symbol, untyped]?) -> String
68
+ def generate(prompt, context: nil)
69
69
 
70
70
  # Splat/double-splat
71
71
  #: (**untyped) -> void
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.16.1"
2
+ ".": "0.17.0"
3
3
  }
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 `tool_context`:
40
+ When the lambda accepts a parameter, it receives the `context`:
41
41
 
42
42
  ```ruby
43
43
  class MyAgent < Riffer::Agent
44
- model ->(ctx) {
45
- ctx&.dig(:premium) ? "anthropic/claude-sonnet-4-20250514" : "anthropic/claude-haiku-4-5-20251001"
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 tool context
270
- response = MyAgent.generate('Look up my orders', tool_context: {user_id: 123})
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 using Ruby's `throw`/`catch` pattern. This is useful for human-in-the-loop approval, cost limits, or content filtering.
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 as the second argument to `throw`. It is available via `interrupt_reason` on the response (generate) or `reason` on the `Interrupt` event (stream):
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
- throw :riffer_interrupt, "needs human approval"
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, tool_context: {user_id: 123})
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, tool_context: {user_id: 123})
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. For each tool call:
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
- throw :riffer_interrupt, "approval needed" if requires_approval?(msg)
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 tool_context passed to agent.generate()
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 `tool_context`:
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", tool_context: {user_id: 123})
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`. However, it's recommended to handle expected errors explicitly for better error messages.
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.
@@ -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.
@@ -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
- ### Tool Context
84
+ ### Context
85
85
 
86
- Pass `tool_context:` to provide context that agents use for dynamic model selection, tool resolution, or tool execution:
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", tool_context: { premium: true } }
93
+ { input: "Premium question", context: { premium: true } }
94
94
  ],
95
95
  evaluators: [AnswerRelevancyEvaluator],
96
- tool_context: { premium: false }
96
+ context: { premium: false }
97
97
  )
98
98
  ```
99
99
 
100
- Per-scenario `tool_context` overrides the top-level value. Scenarios without their own `tool_context` inherit the top-level value.
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", tool_context: {user_id: 123})
124
+ @agent.generate("Do something", context: {user_id: 123})
125
125
 
126
126
  # Tool receives the context
127
127
  end