riffer 0.17.0 → 0.18.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 +41 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +8 -0
- data/docs/03_AGENTS.md +78 -62
- data/docs/04_TOOLS.md +3 -3
- data/docs/06_STREAM_EVENTS.md +1 -1
- data/docs/07_CONFIGURATION.md +8 -8
- data/docs/10_SKILLS.md +166 -0
- data/lib/riffer/agent.rb +143 -75
- data/lib/riffer/providers/anthropic.rb +7 -0
- data/lib/riffer/providers/base.rb +9 -0
- data/lib/riffer/skills/activate_tool.rb +34 -0
- data/lib/riffer/skills/adapter.rb +35 -0
- data/lib/riffer/skills/backend.rb +37 -0
- data/lib/riffer/skills/config.rb +58 -0
- data/lib/riffer/skills/context.rb +76 -0
- data/lib/riffer/skills/filesystem_backend.rb +79 -0
- data/lib/riffer/skills/frontmatter.rb +99 -0
- data/lib/riffer/skills/markdown_adapter.rb +27 -0
- data/lib/riffer/skills/xml_adapter.rb +31 -0
- data/lib/riffer/skills.rb +5 -0
- data/lib/riffer/stream_events/skill_activation.rb +22 -0
- data/lib/riffer/stream_events.rb +1 -0
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/agent.rbs +56 -26
- data/sig/generated/riffer/providers/anthropic.rbs +5 -0
- data/sig/generated/riffer/providers/base.rbs +7 -0
- data/sig/generated/riffer/skills/activate_tool.rbs +17 -0
- data/sig/generated/riffer/skills/adapter.rbs +30 -0
- data/sig/generated/riffer/skills/backend.rbs +32 -0
- data/sig/generated/riffer/skills/config.rbs +44 -0
- data/sig/generated/riffer/skills/context.rbs +54 -0
- data/sig/generated/riffer/skills/filesystem_backend.rbs +41 -0
- data/sig/generated/riffer/skills/frontmatter.rbs +65 -0
- data/sig/generated/riffer/skills/markdown_adapter.rbs +16 -0
- data/sig/generated/riffer/skills/xml_adapter.rbs +15 -0
- data/sig/generated/riffer/skills.rbs +4 -0
- data/sig/generated/riffer/stream_events/skill_activation.rbs +16 -0
- data/sig/generated/riffer/stream_events.rbs +1 -0
- metadata +26 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12ca19d6aeb04239a6938aa523a9e7254a1a5599a9629e6a333fc385a0d94de3
|
|
4
|
+
data.tar.gz: dec4506d7e4800b9cf12aa817e5df14c65b8822930d50ad75b0c912a6d5099bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84b914f6160009c13ba5ac1ed6b659fe45ba13dea14ccb1dd94d529b9de4526c0c7afff84e6bbeac565b81d529e42689c07874da89f6509e4510d59756cad073
|
|
7
|
+
data.tar.gz: df04dc8a8ee28e70b95073b790d268c7488f0a9cd16131e1acafbfa651a9254d8868d9042607a73a5f4572e5e0a5eaee84b27965034894e4022294344d89be1f
|
data/.agents/architecture.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
### Agent (`lib/riffer/agent.rb`)
|
|
6
6
|
|
|
7
|
-
Base class for AI agents. Subclass and use DSL methods `model`, `instructions`, and `
|
|
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
8
|
|
|
9
9
|
```ruby
|
|
10
10
|
class EchoAgent < Riffer::Agent
|
|
@@ -40,6 +40,22 @@ Adapters for LLM APIs. The base class uses a template-method pattern — `genera
|
|
|
40
40
|
|
|
41
41
|
Providers are registered in `Riffer::Providers::Repository::REPO` with identifiers (e.g., `openai`, `amazon_bedrock`).
|
|
42
42
|
|
|
43
|
+
Each provider declares a preferred skill adapter via `self.skills_adapter` (Markdown for most, XML for Anthropic).
|
|
44
|
+
|
|
45
|
+
### Skills (`lib/riffer/skills/`)
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
- `Config` - DSL configuration object (`backend`, `adapter`, `activate`)
|
|
50
|
+
- `Backend` - base class interface (`list_skills`, `read_skill`)
|
|
51
|
+
- `FilesystemBackend` - built-in filesystem scanner
|
|
52
|
+
- `Frontmatter` - parsed YAML frontmatter value object with `.parse(raw)` class method
|
|
53
|
+
- `Context` - coordinates discovery, activation, caching, and prompt rendering for a generation cycle
|
|
54
|
+
- `Adapter` - base class for skill adapters (`render_catalog`, `activate_tool`)
|
|
55
|
+
- `MarkdownAdapter` - default Markdown skill adapter
|
|
56
|
+
- `XmlAdapter` - XML skill adapter for Anthropic/Claude
|
|
57
|
+
- `ActivateTool` - default tool the LLM calls to activate a skill
|
|
58
|
+
|
|
43
59
|
### Messages (`lib/riffer/messages/`)
|
|
44
60
|
|
|
45
61
|
Typed message objects that extend `Riffer::Messages::Base`:
|
|
@@ -65,6 +81,10 @@ Structured events for streaming responses:
|
|
|
65
81
|
- `WebSearchDone` - web search completion with query and sources
|
|
66
82
|
- `Interrupt` - callback interrupted the agent loop
|
|
67
83
|
|
|
84
|
+
### Per-Call State Reset
|
|
85
|
+
|
|
86
|
+
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.
|
|
87
|
+
|
|
68
88
|
### Stopping the Loop Early
|
|
69
89
|
|
|
70
90
|
Two mechanisms can stop the agent loop before the LLM finishes naturally:
|
|
@@ -75,7 +95,15 @@ Two mechanisms can stop the agent loop before the LLM finishes naturally:
|
|
|
75
95
|
|
|
76
96
|
### Resuming After an Interrupt
|
|
77
97
|
|
|
78
|
-
|
|
98
|
+
Two resume paths:
|
|
99
|
+
|
|
100
|
+
- **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.
|
|
101
|
+
- **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`.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
agent.generate('Continue') # in-memory resume
|
|
105
|
+
MyAgent.new.stream(persisted_messages) # cross-process resume
|
|
106
|
+
```
|
|
79
107
|
|
|
80
108
|
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.
|
|
81
109
|
|
|
@@ -105,6 +133,17 @@ lib/
|
|
|
105
133
|
params.rb # Parameter collection with DSL and validation
|
|
106
134
|
structured_output.rb # Structured output schema wrapper
|
|
107
135
|
stream_events.rb # Stream events namespace/module
|
|
136
|
+
skills.rb # Skills namespace/module
|
|
137
|
+
skills/
|
|
138
|
+
config.rb # DSL configuration object
|
|
139
|
+
adapter.rb # Adapter base class (render_catalog, activate_tool)
|
|
140
|
+
markdown_adapter.rb # Default Markdown skill adapter
|
|
141
|
+
xml_adapter.rb # XML skill adapter for Anthropic/Claude
|
|
142
|
+
backend.rb # Backend base class (interface)
|
|
143
|
+
filesystem_backend.rb # Built-in filesystem backend
|
|
144
|
+
frontmatter.rb # Parsed YAML frontmatter value object with .parse
|
|
145
|
+
context.rb # Skills context for a generation cycle
|
|
146
|
+
activate_tool.rb # Default skill_activate tool
|
|
108
147
|
structured_output/
|
|
109
148
|
result.rb # Parse/validation result object
|
|
110
149
|
helpers/
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ 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.18.0](https://github.com/janeapp/riffer/compare/riffer/v0.17.0...riffer/v0.18.0) (2026-03-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add support for agent skills ([#151](https://github.com/janeapp/riffer/issues/151)) ([8847d54](https://github.com/janeapp/riffer/commit/8847d54dede875f207d0e0b28bb64039d3c2e69f))
|
|
14
|
+
* Unify generate/stream API with multi-turn support, remove resume methods ([#165](https://github.com/janeapp/riffer/issues/165)) ([58826df](https://github.com/janeapp/riffer/commit/58826df27806bb10afe1c7ad94322cc643d049f9))
|
|
15
|
+
|
|
8
16
|
## [0.17.0](https://github.com/janeapp/riffer/compare/riffer/v0.16.1...riffer/v0.17.0) (2026-03-06)
|
|
9
17
|
|
|
10
18
|
|
data/docs/03_AGENTS.md
CHANGED
|
@@ -282,27 +282,46 @@ See [Guardrails](09_GUARDRAILS.md) for detailed documentation.
|
|
|
282
282
|
|
|
283
283
|
### generate
|
|
284
284
|
|
|
285
|
-
Generates a response synchronously. Returns a `Riffer::Agent::Response` object
|
|
285
|
+
Generates a response synchronously. Returns a `Riffer::Agent::Response` object.
|
|
286
|
+
|
|
287
|
+
The behavior depends on what you pass and the agent's current state:
|
|
288
|
+
|
|
289
|
+
| Input | Agent state | Behavior |
|
|
290
|
+
| ---------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
291
|
+
| **String** | No prior messages | **New conversation.** Builds system messages (instructions + skills), adds user message, calls the LLM. |
|
|
292
|
+
| **String** | Has messages from a prior call | **Continue conversation.** Appends the user message to the existing history and re-enters the LLM loop. Pending tool calls from a prior interrupt are executed first. |
|
|
293
|
+
| **Array** | No prior messages | **Restore from persisted data.** Uses the array as-is (no system messages added). Pending tool calls are executed. This is for cross-process resume. |
|
|
294
|
+
| **Array** | Has messages from a prior call | **Raises `Riffer::ArgumentError`.** Use a string to continue, or a new agent instance to start from a persisted array. |
|
|
295
|
+
|
|
296
|
+
**State reset per call:** Each call to `generate` or `stream` resets `context`, tools, tool runtime, model, skills state, and the interrupted flag. This means `context:` must be passed on every call — it is not carried over from a previous call. The only state that persists across calls is the message history and cumulative `token_usage`.
|
|
286
297
|
|
|
287
298
|
```ruby
|
|
288
|
-
|
|
299
|
+
agent.generate('Hello', context: {user_id: 123})
|
|
300
|
+
agent.generate('Follow up') # context is nil here — pass it again if needed
|
|
301
|
+
agent.generate('More', context: {user_id: 123}) # context is restored
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
# New conversation (class method — recommended for simple calls)
|
|
289
306
|
response = MyAgent.generate('Hello')
|
|
290
307
|
puts response.content # Access the response text
|
|
291
308
|
puts response.blocked? # Check if guardrail blocked (always false without guardrails)
|
|
292
309
|
puts response.interrupted? # Check if a callback interrupted the loop
|
|
293
310
|
|
|
294
|
-
#
|
|
311
|
+
# New conversation (instance method — when you need message history or callbacks)
|
|
295
312
|
agent = MyAgent.new
|
|
296
313
|
agent.on_message { |msg| log(msg) }
|
|
297
314
|
response = agent.generate('Hello')
|
|
298
315
|
agent.messages # Access message history
|
|
299
316
|
|
|
300
|
-
#
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
317
|
+
# Multi-turn conversation
|
|
318
|
+
agent = MyAgent.new
|
|
319
|
+
agent.generate('Hello')
|
|
320
|
+
agent.generate('Tell me more') # continues with full history
|
|
321
|
+
|
|
322
|
+
# Restore from persisted messages (cross-process resume)
|
|
323
|
+
agent = MyAgent.new
|
|
324
|
+
response = agent.generate(persisted_messages, context: {user_id: 123})
|
|
306
325
|
|
|
307
326
|
# With context
|
|
308
327
|
response = MyAgent.generate('Look up my orders', context: {user_id: 123})
|
|
@@ -322,10 +341,10 @@ response = MyAgent.generate([
|
|
|
322
341
|
|
|
323
342
|
### stream
|
|
324
343
|
|
|
325
|
-
Streams a response as an Enumerator
|
|
344
|
+
Streams a response as an Enumerator. Follows the same input rules as `generate` — a string starts a new conversation or continues an existing one, an array restores from persisted data.
|
|
326
345
|
|
|
327
346
|
```ruby
|
|
328
|
-
#
|
|
347
|
+
# New conversation (class method — recommended for simple calls)
|
|
329
348
|
MyAgent.stream('Tell me a story').each do |event|
|
|
330
349
|
case event
|
|
331
350
|
when Riffer::StreamEvents::TextDelta
|
|
@@ -337,12 +356,17 @@ MyAgent.stream('Tell me a story').each do |event|
|
|
|
337
356
|
end
|
|
338
357
|
end
|
|
339
358
|
|
|
340
|
-
#
|
|
359
|
+
# New conversation (instance method — when you need message history or callbacks)
|
|
341
360
|
agent = MyAgent.new
|
|
342
361
|
agent.on_message { |msg| persist_message(msg) }
|
|
343
362
|
agent.stream('Tell me a story').each { |event| handle(event) }
|
|
344
363
|
agent.messages # Access message history
|
|
345
364
|
|
|
365
|
+
# Multi-turn conversation
|
|
366
|
+
agent = MyAgent.new
|
|
367
|
+
agent.stream('Hello').each { |event| handle(event) }
|
|
368
|
+
agent.stream('Tell me more').each { |event| handle(event) }
|
|
369
|
+
|
|
346
370
|
# With files
|
|
347
371
|
MyAgent.stream('What is in this image?', files: [{data: base64_data, media_type: 'image/jpeg'}]).each do |event|
|
|
348
372
|
print event.content if event.is_a?(Riffer::StreamEvents::TextDelta)
|
|
@@ -428,7 +452,9 @@ end
|
|
|
428
452
|
|
|
429
453
|
#### Resuming an Interrupted Loop
|
|
430
454
|
|
|
431
|
-
|
|
455
|
+
There are two ways to resume after an interrupt, depending on whether the agent is still in memory or you're restoring from persisted data.
|
|
456
|
+
|
|
457
|
+
**In-memory resume** — call `generate` (or `stream`) again with a string. The agent keeps its message history, so a new string appends a user message and continues the loop. Pending tool calls from the interrupt are automatically executed first.
|
|
432
458
|
|
|
433
459
|
```ruby
|
|
434
460
|
agent = MyAgent.new
|
|
@@ -438,53 +464,43 @@ response = agent.generate('Do something risky')
|
|
|
438
464
|
|
|
439
465
|
if response.interrupted?
|
|
440
466
|
approve_action(agent.messages)
|
|
441
|
-
response = agent.
|
|
467
|
+
response = agent.generate('Approved, go ahead') # executes pending tools, then calls the LLM
|
|
442
468
|
end
|
|
443
469
|
```
|
|
444
470
|
|
|
445
|
-
|
|
471
|
+
You can also resume without adding a new user message by passing a continuation like `'Continue'` — the LLM will pick up from the existing context.
|
|
472
|
+
|
|
473
|
+
**Cross-process resume** — when the agent is gone (process restart, async approval, etc.), create a new agent and pass the persisted messages as an array. Array input uses messages as-is (no system messages added) and executes any pending tool calls.
|
|
446
474
|
|
|
447
475
|
```ruby
|
|
448
|
-
#
|
|
476
|
+
# During generation, persist messages via on_message callback
|
|
449
477
|
# Later, in a new process:
|
|
450
478
|
agent = MyAgent.new
|
|
451
|
-
response = agent.
|
|
479
|
+
response = agent.generate(persisted_messages, context: {user_id: 123})
|
|
452
480
|
|
|
453
481
|
# Or resume in streaming mode:
|
|
454
|
-
agent
|
|
482
|
+
agent = MyAgent.new
|
|
483
|
+
agent.stream(persisted_messages).each do |event|
|
|
455
484
|
# handle stream events
|
|
456
485
|
end
|
|
457
486
|
```
|
|
458
487
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
### resume
|
|
462
|
-
|
|
463
|
-
Continues an agent loop synchronously. Returns a `Riffer::Agent::Response` object:
|
|
464
|
-
|
|
465
|
-
```ruby
|
|
466
|
-
# In-memory resume after an interrupt
|
|
467
|
-
response = agent.resume
|
|
488
|
+
**Important:** You cannot pass an array to an agent that already has messages. This raises `Riffer::ArgumentError` because it would silently discard the existing history. Use a string to continue, or create a new agent instance for cross-process resume.
|
|
468
489
|
|
|
469
|
-
|
|
470
|
-
response = agent.resume(messages: persisted_messages, context: {user_id: 123})
|
|
471
|
-
```
|
|
490
|
+
#### Building System Messages for Persistence
|
|
472
491
|
|
|
473
|
-
|
|
492
|
+
Use `generate_instruction_message` and `generate_skills_message` to generate system messages independently. This is useful for database persistence workflows where you need to store and later reconstruct message histories.
|
|
474
493
|
|
|
475
|
-
|
|
494
|
+
Both methods return a `Riffer::Messages::System` or `nil` (when unconfigured). They accept an optional `context:` keyword, just like `generate`.
|
|
476
495
|
|
|
477
496
|
```ruby
|
|
478
|
-
# In-memory resume
|
|
479
|
-
agent.resume_stream.each do |event|
|
|
480
|
-
# handle stream events
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
# Cross-process resume
|
|
484
497
|
agent = MyAgent.new
|
|
485
|
-
agent.
|
|
486
|
-
|
|
487
|
-
|
|
498
|
+
sys = agent.generate_instruction_message(context: ctx) # => Riffer::Messages::System or nil
|
|
499
|
+
skills = agent.generate_skills_message(context: ctx) # => Riffer::Messages::System or nil
|
|
500
|
+
|
|
501
|
+
# Store in DB, then later resume in a new process:
|
|
502
|
+
messages = [sys, skills, user_msg].compact
|
|
503
|
+
MyAgent.new.generate(messages, context: ctx)
|
|
488
504
|
```
|
|
489
505
|
|
|
490
506
|
### interrupt!
|
|
@@ -516,18 +532,18 @@ Returns `nil` if the provider doesn't report usage, or a `Riffer::TokenUsage` ob
|
|
|
516
532
|
|
|
517
533
|
## Response Attributes
|
|
518
534
|
|
|
519
|
-
`Riffer::Agent::Response` is returned by `generate
|
|
535
|
+
`Riffer::Agent::Response` is returned by `generate`:
|
|
520
536
|
|
|
521
|
-
| Attribute
|
|
522
|
-
|
|
|
523
|
-
| `content`
|
|
537
|
+
| Attribute | Type | Description |
|
|
538
|
+
| ------------------- | --------------------------- | -------------------------------------------------- |
|
|
539
|
+
| `content` | `String` | The response text |
|
|
524
540
|
| `structured_output` | `Hash` / `nil` | Parsed and validated structured output (see below) |
|
|
525
|
-
| `blocked?`
|
|
526
|
-
| `tripwire`
|
|
527
|
-
| `modified?`
|
|
528
|
-
| `modifications`
|
|
529
|
-
| `interrupted?`
|
|
530
|
-
| `interrupt_reason`
|
|
541
|
+
| `blocked?` | `Boolean` | `true` if a guardrail tripwire fired |
|
|
542
|
+
| `tripwire` | `Tripwire` / `nil` | The guardrail tripwire that blocked the request |
|
|
543
|
+
| `modified?` | `Boolean` | `true` if a guardrail modified the content |
|
|
544
|
+
| `modifications` | `Array` | List of guardrail modifications applied |
|
|
545
|
+
| `interrupted?` | `Boolean` | `true` if the loop was interrupted |
|
|
546
|
+
| `interrupt_reason` | `String` / `Symbol` / `nil` | The reason passed to `throw :riffer_interrupt` |
|
|
531
547
|
|
|
532
548
|
### response.structured_output
|
|
533
549
|
|
|
@@ -629,7 +645,7 @@ Callbacks registered with `on_message` can call `agent.interrupt!` (or `throw :r
|
|
|
629
645
|
- **When to use:** Flow control that depends on runtime decisions — human-in-the-loop approval, budget tracking, conditional pausing.
|
|
630
646
|
- **Response:** `response.interrupted?` returns `true`, `response.interrupt_reason` contains the optional reason.
|
|
631
647
|
- **Streaming:** Yields an `Interrupt` event with a `reason` attribute.
|
|
632
|
-
- **Resumable:** Yes. Call `
|
|
648
|
+
- **Resumable:** Yes. Call `generate('Continue')` or `stream('Continue')` on the same agent instance to resume. For cross-process resume, pass persisted messages as an array to a new agent. Pending tool calls are automatically executed before the LLM loop resumes.
|
|
633
649
|
|
|
634
650
|
```ruby
|
|
635
651
|
agent = MyAgent.new
|
|
@@ -640,7 +656,7 @@ end
|
|
|
640
656
|
response = agent.generate('Do something risky')
|
|
641
657
|
response.interrupted? # => true
|
|
642
658
|
response.interrupt_reason # => "approval needed"
|
|
643
|
-
response = agent.
|
|
659
|
+
response = agent.generate('Approved, continue') # continues where it left off
|
|
644
660
|
```
|
|
645
661
|
|
|
646
662
|
### Max Steps Limit
|
|
@@ -650,7 +666,7 @@ The `max_steps` class method caps the number of LLM call steps in the tool-use l
|
|
|
650
666
|
- **When to use:** Safety net to prevent runaway tool-use loops — useful when agents have access to many tools or operate autonomously.
|
|
651
667
|
- **Response:** `response.interrupted?` returns `true`, `response.interrupt_reason` is `:max_steps`.
|
|
652
668
|
- **Streaming:** Yields an `Interrupt` event with `reason: :max_steps`.
|
|
653
|
-
- **Resumable:** Yes. Call `
|
|
669
|
+
- **Resumable:** Yes. Call `generate('Continue')` or `stream('Continue')` on the same agent instance to resume. For cross-process resume, pass persisted messages as an array to a new agent. Pending tool calls are automatically executed before the LLM loop resumes.
|
|
654
670
|
|
|
655
671
|
```ruby
|
|
656
672
|
class MyAgent < Riffer::Agent
|
|
@@ -669,11 +685,11 @@ If a guardrail, provider call, or other internal code raises an exception, it pr
|
|
|
669
685
|
|
|
670
686
|
### Comparison
|
|
671
687
|
|
|
672
|
-
| | Guardrail Tripwire | Callback Interrupt
|
|
673
|
-
| ------------- | ------------------------------------ |
|
|
674
|
-
| Defined | At class level (`guardrail :before`) | At instance level (`on_message`)
|
|
675
|
-
| Fires | Automatically on every request | When callback logic decides
|
|
676
|
-
| Resumable | No | Yes (`
|
|
677
|
-
| Response flag | `blocked?` | `interrupted?`
|
|
678
|
-
| Stream event | `GuardrailTripwire` | `Interrupt`
|
|
679
|
-
| Purpose | Policy enforcement | Flow control
|
|
688
|
+
| | Guardrail Tripwire | Callback Interrupt | Max Steps Limit |
|
|
689
|
+
| ------------- | ------------------------------------ | ------------------------------------ | ------------------------------------ |
|
|
690
|
+
| Defined | At class level (`guardrail :before`) | At instance level (`on_message`) | At class level (`max_steps 8`) |
|
|
691
|
+
| Fires | Automatically on every request | When callback logic decides | When step count reaches limit |
|
|
692
|
+
| Resumable | No | Yes (call `generate`/`stream` again) | Yes (call `generate`/`stream` again) |
|
|
693
|
+
| Response flag | `blocked?` | `interrupted?` | `interrupted?` |
|
|
694
|
+
| Stream event | `GuardrailTripwire` | `Interrupt` | `Interrupt` |
|
|
695
|
+
| Purpose | Policy enforcement | Flow control | Runaway loop prevention |
|
data/docs/04_TOOLS.md
CHANGED
|
@@ -380,9 +380,9 @@ By default, tool calls are executed sequentially in the current thread using `Ri
|
|
|
380
380
|
|
|
381
381
|
### Built-in Runtimes
|
|
382
382
|
|
|
383
|
-
| Runtime
|
|
384
|
-
|
|
385
|
-
| `Riffer::ToolRuntime::Inline`
|
|
383
|
+
| Runtime | Description |
|
|
384
|
+
| ------------------------------- | ---------------------------------------------- |
|
|
385
|
+
| `Riffer::ToolRuntime::Inline` | Executes tool calls sequentially (default) |
|
|
386
386
|
| `Riffer::ToolRuntime::Threaded` | Executes tool calls concurrently using threads |
|
|
387
387
|
|
|
388
388
|
### Per-Agent Configuration
|
data/docs/06_STREAM_EVENTS.md
CHANGED
|
@@ -218,7 +218,7 @@ agent.stream("Hello").each do |event|
|
|
|
218
218
|
end
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
After an interrupt,
|
|
221
|
+
After an interrupt, call `stream` again with a string to continue the loop. See [Agents — Resuming an Interrupted Loop](03_AGENTS.md#resuming-an-interrupted-loop) for details.
|
|
222
222
|
|
|
223
223
|
### TokenUsageDone
|
|
224
224
|
|
data/docs/07_CONFIGURATION.md
CHANGED
|
@@ -84,11 +84,11 @@ Riffer.configure do |config|
|
|
|
84
84
|
end
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
| Value
|
|
88
|
-
|
|
87
|
+
| Value | Description |
|
|
88
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
89
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`
|
|
90
|
+
| `Riffer::ToolRuntime` instance | Custom runtime with specific options |
|
|
91
|
+
| `Proc` | Dynamic resolution |
|
|
92
92
|
|
|
93
93
|
Per-agent configuration overrides this global default. See [Tools — Tool Runtime](04_TOOLS.md#tool-runtime-experimental) for details.
|
|
94
94
|
|
|
@@ -145,10 +145,10 @@ end
|
|
|
145
145
|
|
|
146
146
|
Options are passed through to the [Bedrock Converse API](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/BedrockRuntime/Client.html#converse-instance_method).
|
|
147
147
|
|
|
148
|
-
| Option
|
|
149
|
-
|
|
|
150
|
-
| `inference_config`
|
|
151
|
-
| `additional_model_request_fields`
|
|
148
|
+
| Option | Description |
|
|
149
|
+
| --------------------------------- | ---------------------------------------------------------------- |
|
|
150
|
+
| `inference_config` | Hash with `max_tokens`, `temperature`, `top_p`, `stop_sequences` |
|
|
151
|
+
| `additional_model_request_fields` | Hash for model-specific params (e.g., `top_k` for Claude) |
|
|
152
152
|
|
|
153
153
|
```ruby
|
|
154
154
|
class MyAgent < Riffer::Agent
|
data/docs/10_SKILLS.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Skills
|
|
2
|
+
|
|
3
|
+
Skills are packaged AI agent capabilities per the [Agent Skills spec](https://agentskills.io/). Each skill is a directory containing a `SKILL.md` file with YAML frontmatter and Markdown instructions. The framework discovers skills through a pluggable backend, injects a compact catalog into the system prompt (~50 tokens/skill), and provides a tool for the LLM to activate skills on demand.
|
|
4
|
+
|
|
5
|
+
## Creating a Skill
|
|
6
|
+
|
|
7
|
+
Create a directory with a `SKILL.md` file:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.skills/
|
|
11
|
+
code-review/
|
|
12
|
+
SKILL.md
|
|
13
|
+
data-analysis/
|
|
14
|
+
SKILL.md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Each `SKILL.md` has YAML frontmatter and a Markdown body:
|
|
18
|
+
|
|
19
|
+
```markdown
|
|
20
|
+
---
|
|
21
|
+
name: code-review
|
|
22
|
+
description: Reviews code for quality, style, and potential issues.
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
You are a code review assistant.
|
|
26
|
+
|
|
27
|
+
Review the code for:
|
|
28
|
+
|
|
29
|
+
- Style issues
|
|
30
|
+
- Potential bugs
|
|
31
|
+
- Performance problems
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Required frontmatter fields:**
|
|
35
|
+
|
|
36
|
+
- `name` — lowercase alphanumeric with hyphens, 1-64 chars (must match directory name)
|
|
37
|
+
- `description` — 1-1024 chars, helps the LLM decide when to activate
|
|
38
|
+
|
|
39
|
+
Additional frontmatter keys are passed through as metadata.
|
|
40
|
+
|
|
41
|
+
## Configuring an Agent
|
|
42
|
+
|
|
43
|
+
Use the `skills` block DSL to configure skills:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class MyAgent < Riffer::Agent
|
|
47
|
+
model "openai/gpt-4o"
|
|
48
|
+
instructions "You are a helpful assistant."
|
|
49
|
+
skills do
|
|
50
|
+
backend Riffer::Skills::FilesystemBackend.new(".skills")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Multiple directories can be scanned (first-path-wins for duplicates):
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
skills do
|
|
59
|
+
backend Riffer::Skills::FilesystemBackend.new(".skills", "~/.riffer/skills")
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Dynamic Backend via Proc
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
skills do
|
|
67
|
+
backend ->(context) { tenant_backend(context[:tenant_id]) }
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Custom Adapter
|
|
72
|
+
|
|
73
|
+
The adapter controls how the skill catalog is rendered in the system prompt and which tool the LLM calls to activate a skill. The adapter is auto-selected by provider (Markdown for most, XML for Anthropic). Override with:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
skills do
|
|
77
|
+
backend Riffer::Skills::FilesystemBackend.new(".skills")
|
|
78
|
+
adapter Riffer::Skills::XmlAdapter
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Activated Skills
|
|
83
|
+
|
|
84
|
+
Load skill instructions into the system prompt at startup (no tool call needed):
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
skills do
|
|
88
|
+
backend Riffer::Skills::FilesystemBackend.new(".skills")
|
|
89
|
+
activate ["code-review"]
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Accepts a Proc for dynamic resolution:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
skills do
|
|
97
|
+
backend Riffer::Skills::FilesystemBackend.new(".skills")
|
|
98
|
+
activate ->(context) { context[:active_skills] || [] }
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## How It Works
|
|
103
|
+
|
|
104
|
+
1. **Discovery** — At the start of `generate`/`stream`, the backend's `list_skills` returns frontmatter for all available skills.
|
|
105
|
+
2. **Catalog injection** — The adapter formats the catalog and appends it to the system prompt.
|
|
106
|
+
3. **Activation** — When the LLM matches a task to a skill, it calls the `skill_activate` tool with the skill name. The tool returns the full SKILL.md body.
|
|
107
|
+
4. **Execution** — The LLM follows the skill's instructions to complete the task.
|
|
108
|
+
|
|
109
|
+
## Custom Backends
|
|
110
|
+
|
|
111
|
+
Implement `Riffer::Skills::Backend` for non-filesystem storage:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class DatabaseBackend < Riffer::Skills::Backend
|
|
115
|
+
def list_skills
|
|
116
|
+
# Return Array[Riffer::Skills::Frontmatter]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def read_skill(name)
|
|
120
|
+
# Return String (skill body)
|
|
121
|
+
# Raise Riffer::ArgumentError if not found
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Custom Adapters
|
|
127
|
+
|
|
128
|
+
Subclass `Riffer::Skills::Adapter` to customize how the skill catalog is rendered and which tool the LLM uses to activate skills:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
class CustomAdapter < Riffer::Skills::Adapter
|
|
132
|
+
def render_catalog(skills)
|
|
133
|
+
# Return String (skill catalog for the system prompt)
|
|
134
|
+
# Use `activate_tool.name` to reference the activation tool
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def activate_tool
|
|
138
|
+
# Return a Riffer::Tool subclass
|
|
139
|
+
# Defaults to Riffer::Skills::ActivateTool
|
|
140
|
+
Riffer::Skills::ActivateTool
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The built-in adapters are `Riffer::Skills::MarkdownAdapter` (default) and `Riffer::Skills::XmlAdapter` (used by Anthropic).
|
|
146
|
+
|
|
147
|
+
## Accessing Skills in Tools
|
|
148
|
+
|
|
149
|
+
The skills context (`Riffer::Skills::Context`) is available via `context[:skills]` during execution:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
class SkillSearchTool < Riffer::Tool
|
|
153
|
+
identifier "skill_search"
|
|
154
|
+
description "Searches available skills."
|
|
155
|
+
|
|
156
|
+
params do
|
|
157
|
+
required :query, String
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def call(context:, query:)
|
|
161
|
+
skills_context = context[:skills] # Riffer::Skills::Context
|
|
162
|
+
matches = skills_context.skills.values.select { |s| s.description.include?(query) }
|
|
163
|
+
json(matches.map { |s| {name: s.name, description: s.description} })
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|