riffer 0.28.0 → 0.29.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 +18 -11
- data/.agents/code-style.md +1 -1
- data/.agents/rbs-inline.md +2 -2
- data/.agents/testing.md +9 -5
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +17 -10
- data/CHANGELOG.md +19 -0
- data/README.md +17 -18
- data/Steepfile +7 -1
- data/docs/03_AGENTS.md +34 -3
- data/docs/04_AGENT_LIFECYCLE.md +87 -86
- data/docs/05_AGENT_LOOP.md +2 -2
- data/docs/06_TOOLS.md +9 -4
- data/docs/07_TOOL_ADVANCED.md +17 -17
- data/docs/08_MESSAGES.md +25 -32
- data/docs/09_STREAM_EVENTS.md +1 -1
- data/docs/10_CONFIGURATION.md +7 -18
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/06_MOCK_PROVIDER.md +2 -1
- data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
- data/docs/providers/08_GEMINI.md +2 -2
- data/docs/providers/09_OPENROUTER.md +242 -0
- data/lib/riffer/agent/config.rb +173 -0
- data/lib/riffer/agent/context.rb +125 -0
- data/lib/riffer/agent/run.rb +308 -0
- data/lib/riffer/agent/session/repair.rb +112 -0
- data/lib/riffer/agent/session.rb +268 -0
- data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
- data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
- data/lib/riffer/agent.rb +234 -923
- data/lib/riffer/config.rb +14 -7
- data/lib/riffer/evals/evaluator.rb +13 -3
- data/lib/riffer/evals/judge.rb +2 -2
- data/lib/riffer/evals/run_result.rb +2 -1
- data/lib/riffer/evals/scenario_result.rb +2 -1
- data/lib/riffer/guardrails/runner.rb +3 -2
- data/lib/riffer/helpers/call_or_value.rb +16 -0
- data/lib/riffer/helpers.rb +0 -1
- data/lib/riffer/mcp/authenticated_tool.rb +4 -0
- data/lib/riffer/mcp/client.rb +1 -1
- data/lib/riffer/mcp/registration.rb +2 -3
- data/lib/riffer/mcp/registry.rb +3 -1
- data/lib/riffer/mcp/tool_factory.rb +5 -0
- data/lib/riffer/messages/assistant.rb +9 -3
- data/lib/riffer/messages/base.rb +22 -0
- data/lib/riffer/messages/converter.rb +6 -6
- data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
- data/lib/riffer/messages/tool.rb +1 -1
- data/lib/riffer/messages/user.rb +4 -4
- data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
- data/lib/riffer/{param.rb → params/param.rb} +6 -6
- data/lib/riffer/params.rb +27 -21
- data/lib/riffer/providers/amazon_bedrock.rb +19 -20
- data/lib/riffer/providers/anthropic.rb +27 -28
- data/lib/riffer/providers/base.rb +10 -9
- data/lib/riffer/providers/gemini.rb +15 -12
- data/lib/riffer/providers/mock.rb +41 -13
- data/lib/riffer/providers/open_ai.rb +24 -22
- data/lib/riffer/providers/open_router.rb +318 -0
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
- data/lib/riffer/providers.rb +1 -0
- data/lib/riffer/runner/fibers.rb +4 -3
- data/lib/riffer/runner/sequential.rb +1 -1
- data/lib/riffer/runner/threaded.rb +1 -1
- data/lib/riffer/runner.rb +1 -1
- data/lib/riffer/skills/activate_tool.rb +4 -3
- data/lib/riffer/skills/config.rb +1 -1
- data/lib/riffer/skills/context.rb +3 -3
- data/lib/riffer/skills/filesystem_backend.rb +7 -5
- data/lib/riffer/skills/markdown_adapter.rb +1 -1
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/interrupt.rb +1 -1
- data/lib/riffer/stream_events/token_usage_done.rb +2 -2
- data/lib/riffer/stream_events/web_search_status.rb +1 -1
- data/lib/riffer/tool.rb +3 -3
- data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
- data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
- data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
- data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +9 -9
- data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -1
- data/sig/generated/riffer/agent/config.rbs +119 -0
- data/sig/generated/riffer/agent/context.rbs +91 -0
- data/sig/generated/riffer/agent/run.rbs +144 -0
- data/sig/generated/riffer/agent/session/repair.rbs +51 -0
- data/sig/generated/riffer/agent/session.rbs +145 -0
- data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
- data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
- data/sig/generated/riffer/agent.rbs +143 -342
- data/sig/generated/riffer/config.rbs +17 -5
- data/sig/generated/riffer/evals/judge.rbs +2 -2
- data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
- data/sig/generated/riffer/helpers.rbs +0 -1
- data/sig/generated/riffer/messages/assistant.rbs +7 -3
- data/sig/generated/riffer/messages/base.rbs +18 -0
- data/sig/generated/riffer/messages/converter.rbs +4 -4
- data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
- data/sig/generated/riffer/messages/user.rbs +4 -4
- data/sig/generated/riffer/params/boolean.rbs +10 -0
- data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
- data/sig/generated/riffer/params.rbs +15 -15
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
- data/sig/generated/riffer/providers/anthropic.rbs +4 -4
- data/sig/generated/riffer/providers/base.rbs +10 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +25 -5
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +85 -0
- data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
- data/sig/generated/riffer/providers.rbs +1 -0
- data/sig/generated/riffer/runner/fibers.rbs +2 -2
- data/sig/generated/riffer/runner/sequential.rbs +2 -2
- data/sig/generated/riffer/runner/threaded.rbs +2 -2
- data/sig/generated/riffer/runner.rbs +2 -2
- data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
- data/sig/generated/riffer/skills/config.rbs +1 -1
- data/sig/generated/riffer/skills/context.rbs +2 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
- data/sig/generated/riffer/tool.rbs +5 -5
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +12 -12
- data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
- data/sig/stubs/agent_ivars.rbs +7 -0
- data/sig/stubs/async.rbs +24 -0
- data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
- data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
- data/sig/stubs/extend_self.rbs +11 -0
- data/sig/stubs/lib_ivars.rbs +101 -0
- data/sig/stubs/mcp_sdk.rbs +22 -0
- data/sig/stubs/provider_ivars.rbs +36 -0
- data/sig/stubs/provider_sdk_methods.rbs +50 -0
- data/sig/stubs/zeitwerk.rbs +12 -0
- metadata +54 -33
- data/lib/riffer/core.rb +0 -28
- data/lib/riffer/helpers/validations.rb +0 -18
- data/sig/generated/riffer/boolean.rbs +0 -10
- data/sig/generated/riffer/core.rbs +0 -19
- data/sig/generated/riffer/helpers/validations.rbs +0 -12
data/docs/04_AGENT_LIFECYCLE.md
CHANGED
|
@@ -1,69 +1,64 @@
|
|
|
1
1
|
# Agent Lifecycle
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Construction
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Agent.new
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```ruby
|
|
8
|
+
Agent.new(session: nil, context: nil)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **`session:`** — an existing `Riffer::Agent::Session`. When given, the agent uses it as-is (no system/skills seeding). Typical use case: cross-process resume from persisted history. With `Riffer.config.experimental_history_healing` on, a provided session is healed at construction time so the `tool_use` ↔ `tool_result` invariant holds before the next inference call.
|
|
12
|
+
- **`context:`** — a `Hash` carried for the lifetime of the agent. Used to evaluate Proc-based `instructions`, `model`, `uses_tools`, and skill activation at construction time, and threaded through tool execution and guardrails on every `generate`/`stream` call.
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
When `session:` is omitted, the agent constructs a fresh session and seeds it with `[instruction_message, skills_message].compact` eagerly. To swap context, construct a new agent — context is fixed for the lifetime of an agent instance.
|
|
15
|
+
|
|
16
|
+
## Instance Methods
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
| ---------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
-
| **String** | No prior messages | **New conversation.** Builds system messages (instructions + skills), adds user message, calls the LLM. |
|
|
14
|
-
| **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. |
|
|
15
|
-
| **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. |
|
|
16
|
-
| **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. |
|
|
18
|
+
### generate
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Generates a response synchronously. Returns a `Riffer::Agent::Response` object.
|
|
19
21
|
|
|
20
22
|
```ruby
|
|
21
|
-
agent.generate(
|
|
22
|
-
agent.generate('Follow up') # context is nil here — pass it again if needed
|
|
23
|
-
agent.generate('More', context: {user_id: 123}) # context is restored
|
|
23
|
+
agent.generate(prompt = nil, files: nil)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
- **`prompt`** — when given, a new `Riffer::Messages::User` is silently appended to the session (no `on_message` callbacks fire for user inputs) and the inference loop runs.
|
|
27
|
+
- **`prompt` omitted** — the loop runs against the current session. Useful when the seeded session's last turn is already a user message, or when picking up pending tool calls from a prior interrupt.
|
|
28
|
+
- **`files:`** — requires `prompt`. Attached to the new user message.
|
|
29
|
+
|
|
26
30
|
```ruby
|
|
27
31
|
# New conversation (class method — recommended for simple calls)
|
|
28
|
-
response = MyAgent.generate('Hello')
|
|
32
|
+
response = MyAgent.generate('Hello', context: {user_id: 123})
|
|
29
33
|
puts response.content # Access the response text
|
|
30
34
|
puts response.blocked? # Check if guardrail blocked (always false without guardrails)
|
|
31
35
|
puts response.interrupted? # Check if a callback interrupted the loop
|
|
32
36
|
|
|
33
37
|
# New conversation (instance method — when you need message history or callbacks)
|
|
34
|
-
agent = MyAgent.new
|
|
35
|
-
agent.on_message { |msg| log(msg) }
|
|
38
|
+
agent = MyAgent.new(context: {user_id: 123})
|
|
39
|
+
agent.session.on_message { |msg| log(msg) }
|
|
36
40
|
response = agent.generate('Hello')
|
|
37
|
-
agent.messages # Access message history
|
|
41
|
+
agent.session.messages # Access message history
|
|
38
42
|
|
|
39
43
|
# Multi-turn conversation
|
|
40
44
|
agent = MyAgent.new
|
|
41
45
|
agent.generate('Hello')
|
|
42
46
|
agent.generate('Tell me more') # continues with full history
|
|
43
47
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# With context
|
|
49
|
-
response = MyAgent.generate('Look up my orders', context: {user_id: 123})
|
|
48
|
+
# Resume from persisted messages (cross-process)
|
|
49
|
+
session = Riffer::Agent::Session.new(messages: persisted_messages)
|
|
50
|
+
agent = MyAgent.new(session: session, context: {user_id: 123})
|
|
51
|
+
response = agent.generate # no prompt — session already has the last user message
|
|
50
52
|
|
|
51
|
-
# With files
|
|
53
|
+
# With files
|
|
52
54
|
response = MyAgent.generate('What is in this image?', files: [
|
|
53
55
|
{data: base64_data, media_type: 'image/jpeg'}
|
|
54
56
|
])
|
|
55
|
-
|
|
56
|
-
# With files in messages array (per-message)
|
|
57
|
-
response = MyAgent.generate([
|
|
58
|
-
{role: 'user', content: 'Describe this document', files: [
|
|
59
|
-
{url: 'https://example.com/report.pdf', media_type: 'application/pdf'}
|
|
60
|
-
]}
|
|
61
|
-
])
|
|
62
57
|
```
|
|
63
58
|
|
|
64
59
|
### stream
|
|
65
60
|
|
|
66
|
-
Streams a response as an Enumerator.
|
|
61
|
+
Streams a response as an Enumerator. Same prompt/files semantics as `generate`.
|
|
67
62
|
|
|
68
63
|
```ruby
|
|
69
64
|
# New conversation (class method — recommended for simple calls)
|
|
@@ -80,9 +75,9 @@ end
|
|
|
80
75
|
|
|
81
76
|
# New conversation (instance method — when you need message history or callbacks)
|
|
82
77
|
agent = MyAgent.new
|
|
83
|
-
agent.on_message { |msg| persist_message(msg) }
|
|
78
|
+
agent.session.on_message { |msg| persist_message(msg) }
|
|
84
79
|
agent.stream('Tell me a story').each { |event| handle(event) }
|
|
85
|
-
agent.messages # Access message history
|
|
80
|
+
agent.session.messages # Access message history
|
|
86
81
|
|
|
87
82
|
# Multi-turn conversation
|
|
88
83
|
agent = MyAgent.new
|
|
@@ -95,7 +90,9 @@ MyAgent.stream('What is in this image?', files: [{data: base64_data, media_type:
|
|
|
95
90
|
end
|
|
96
91
|
```
|
|
97
92
|
|
|
98
|
-
###
|
|
93
|
+
### session
|
|
94
|
+
|
|
95
|
+
Conversation state lives on `agent.session` — a `Riffer::Agent::Session` instance that owns the message array, the `on_message` callback list, and the `tool_use` ↔ `tool_result` invariant. The methods below are all on the session, not on the agent itself.
|
|
99
96
|
|
|
100
97
|
Access the message history after a generate/stream call:
|
|
101
98
|
|
|
@@ -103,17 +100,24 @@ Access the message history after a generate/stream call:
|
|
|
103
100
|
agent = MyAgent.new
|
|
104
101
|
agent.generate('Hello')
|
|
105
102
|
|
|
106
|
-
agent.messages.each do |msg|
|
|
103
|
+
agent.session.messages.each do |msg|
|
|
107
104
|
puts "#{msg.role}: #{msg.content}"
|
|
108
105
|
end
|
|
109
106
|
```
|
|
110
107
|
|
|
108
|
+
`Riffer::Agent::Session` includes `Enumerable`, so `find`, `select`, `count`, `reverse_each` all work directly on the session:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
agent.session.find { |m| m.id == 'a_1' }
|
|
112
|
+
agent.session.count { |m| m.is_a?(Riffer::Messages::Assistant) }
|
|
113
|
+
```
|
|
114
|
+
|
|
111
115
|
### on_message
|
|
112
116
|
|
|
113
117
|
Registers a callback to receive messages as they're added during generation:
|
|
114
118
|
|
|
115
119
|
```ruby
|
|
116
|
-
agent.on_message do |message|
|
|
120
|
+
agent.session.on_message do |message|
|
|
117
121
|
case message.role
|
|
118
122
|
when :assistant
|
|
119
123
|
puts "[Assistant] #{message.content}"
|
|
@@ -126,10 +130,10 @@ end
|
|
|
126
130
|
Multiple callbacks can be registered. Returns `self` for method chaining:
|
|
127
131
|
|
|
128
132
|
```ruby
|
|
129
|
-
agent
|
|
133
|
+
agent.session
|
|
130
134
|
.on_message { |msg| persist_message(msg) }
|
|
131
135
|
.on_message { |msg| log_message(msg) }
|
|
132
|
-
|
|
136
|
+
agent.generate('Hello')
|
|
133
137
|
```
|
|
134
138
|
|
|
135
139
|
Works with both `generate` and `stream`. Only emits agent-generated messages (Assistant, Tool), not inputs (System, User).
|
|
@@ -144,7 +148,7 @@ An optional reason can be passed to `interrupt!`. It is available via `interrupt
|
|
|
144
148
|
|
|
145
149
|
```ruby
|
|
146
150
|
agent = MyAgent.new
|
|
147
|
-
agent.on_message do |msg|
|
|
151
|
+
agent.session.on_message do |msg|
|
|
148
152
|
if msg.is_a?(Riffer::Messages::Tool)
|
|
149
153
|
agent.interrupt!("needs human approval")
|
|
150
154
|
end
|
|
@@ -160,7 +164,7 @@ response.content # => last assistant content before interrupt
|
|
|
160
164
|
|
|
161
165
|
```ruby
|
|
162
166
|
agent = MyAgent.new
|
|
163
|
-
agent.on_message { |msg| throw :riffer_interrupt, "budget exceeded" }
|
|
167
|
+
agent.session.on_message { |msg| throw :riffer_interrupt, "budget exceeded" }
|
|
164
168
|
|
|
165
169
|
agent.stream('Hello').each do |event|
|
|
166
170
|
case event
|
|
@@ -176,53 +180,51 @@ end
|
|
|
176
180
|
|
|
177
181
|
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.
|
|
178
182
|
|
|
179
|
-
**In-memory resume** — call `generate` (or `stream`) again
|
|
183
|
+
**In-memory resume** — call `generate` (or `stream`) again. With a prompt, the new user message is appended and the loop runs. Without a prompt, the loop runs against the current session — useful for picking up pending tool calls after the user has approved.
|
|
180
184
|
|
|
181
185
|
```ruby
|
|
182
|
-
agent = MyAgent.new
|
|
183
|
-
agent.on_message { |msg| throw :riffer_interrupt if needs_approval?(msg) }
|
|
186
|
+
agent = MyAgent.new(context: {user_id: 123})
|
|
187
|
+
agent.session.on_message { |msg| throw :riffer_interrupt if needs_approval?(msg) }
|
|
184
188
|
|
|
185
189
|
response = agent.generate('Do something risky')
|
|
186
190
|
|
|
187
191
|
if response.interrupted?
|
|
188
|
-
approve_action(agent.messages)
|
|
192
|
+
approve_action(agent.session.messages)
|
|
189
193
|
response = agent.generate('Approved, go ahead') # executes pending tools, then calls the LLM
|
|
194
|
+
# or: agent.generate # resume without a new turn
|
|
190
195
|
end
|
|
191
196
|
```
|
|
192
197
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
**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.
|
|
198
|
+
**Cross-process resume** — when the agent is gone (process restart, async approval, etc.), construct a `Riffer::Agent::Session` from the persisted messages and pass it to a new agent. The agent uses the session as-is (no system messages added). Pending tool calls on the resume boundary are executed on the next `generate`/`stream`.
|
|
196
199
|
|
|
197
200
|
```ruby
|
|
198
|
-
# During generation, persist
|
|
201
|
+
# During generation, persist each new message via on_message
|
|
199
202
|
# Later, in a new process:
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
session = Riffer::Agent::Session.new(messages: persisted_messages)
|
|
204
|
+
agent = MyAgent.new(session: session, context: {user_id: 123})
|
|
205
|
+
response = agent.generate # session already has the last user turn
|
|
202
206
|
|
|
203
207
|
# Or resume in streaming mode:
|
|
204
|
-
agent = MyAgent.new
|
|
205
|
-
agent.stream
|
|
208
|
+
agent = MyAgent.new(session: session, context: {user_id: 123})
|
|
209
|
+
agent.stream.each do |event|
|
|
206
210
|
# handle stream events
|
|
207
211
|
end
|
|
208
212
|
```
|
|
209
213
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
### Building System Messages for Persistence
|
|
214
|
+
### Reading System Messages for Persistence
|
|
213
215
|
|
|
214
|
-
|
|
216
|
+
Read the agent's instruction and skills system messages from `agent.instruction_message` and `agent.skills_message`. Both are built once at `Agent.new` time using the constructor `context:` and cached — they reflect the agent's configured `instructions` and `skills` DSL output. Useful for database persistence workflows where you need to store and later reconstruct message histories.
|
|
215
217
|
|
|
216
|
-
Both
|
|
218
|
+
Both return `Riffer::Messages::System` or `nil` (when unconfigured / empty).
|
|
217
219
|
|
|
218
220
|
```ruby
|
|
219
|
-
agent = MyAgent.new
|
|
220
|
-
sys = agent.
|
|
221
|
-
skills = agent.
|
|
221
|
+
agent = MyAgent.new(context: ctx)
|
|
222
|
+
sys = agent.instruction_message # => Riffer::Messages::System or nil
|
|
223
|
+
skills = agent.skills_message # => Riffer::Messages::System or nil
|
|
222
224
|
|
|
223
225
|
# Store in DB, then later resume in a new process:
|
|
224
|
-
|
|
225
|
-
MyAgent.new
|
|
226
|
+
session = Riffer::Agent::Session.new(messages: [sys, skills, user_msg].compact)
|
|
227
|
+
MyAgent.new(session: session, context: ctx).generate
|
|
226
228
|
```
|
|
227
229
|
|
|
228
230
|
### interrupt!
|
|
@@ -230,7 +232,7 @@ MyAgent.new.generate(messages, context: ctx)
|
|
|
230
232
|
Interrupts the agent loop from an `on_message` callback. Equivalent to `throw :riffer_interrupt, reason`:
|
|
231
233
|
|
|
232
234
|
```ruby
|
|
233
|
-
agent.on_message do |msg|
|
|
235
|
+
agent.session.on_message do |msg|
|
|
234
236
|
agent.interrupt!(:needs_approval) if requires_approval?(msg)
|
|
235
237
|
end
|
|
236
238
|
```
|
|
@@ -244,7 +246,7 @@ When the interrupt represents a course-change rather than a pause — e.g. a voi
|
|
|
244
246
|
```ruby
|
|
245
247
|
Riffer.configure { |c| c.experimental_history_healing = true }
|
|
246
248
|
|
|
247
|
-
agent.on_message do |msg|
|
|
249
|
+
agent.session.on_message do |msg|
|
|
248
250
|
agent.interrupt!(:user_interrupt) if msg.is_a?(Riffer::Messages::Assistant) && barge_in?
|
|
249
251
|
end
|
|
250
252
|
|
|
@@ -256,48 +258,47 @@ The placeholder content is fixed: `"Tool call interrupted before completion."` w
|
|
|
256
258
|
|
|
257
259
|
Healing covers all interrupts uniformly — caller-issued `interrupt!` and the built-in `INTERRUPT_MAX_STEPS` ceiling alike. When the flag is off (the default), orphans remain in history and `execute_pending_tool_calls` re-runs them on the next `generate` call.
|
|
258
260
|
|
|
259
|
-
If you need finer control over placeholder content (per-call shape, structured metadata, etc.), use the `
|
|
261
|
+
If you need finer control over placeholder content (per-call shape, structured metadata, etc.), use the `update` mutator below to upgrade a placeholder after the interrupt returns.
|
|
260
262
|
|
|
261
263
|
### Mutating history
|
|
262
264
|
|
|
263
|
-
The
|
|
265
|
+
The session exposes a small set of in-place mutators that enforce the `tool_use` ↔ `tool_result` invariant on every operation. Use these to align history with external state (persisted transcript, partial output that wasn't actually delivered, etc.) without rebuilding the agent.
|
|
264
266
|
|
|
265
|
-
- **`agent.
|
|
266
|
-
- **`agent.
|
|
267
|
-
- **`agent.
|
|
267
|
+
- **`agent.session.update(id:, **attrs)`** — In-place partial update. Looks up by message `id:`; builds a replacement of the same type with `attrs` overlaid on the existing fields. Use this to edit assistant content (`update(id:, content:)`), restate a system message, etc. When the target is an assistant and the update drops entries from `tool_calls`, matching `Tool` children are removed atomically.
|
|
268
|
+
- **`agent.session.update(tool_call_id:, **attrs)`** — Same as above but looks up the tool result by `tool_call_id:`. Preserves `name` and `id`. Use this to upgrade an interrupt-time placeholder once the real result is available (`update(tool_call_id:, content:, error: nil, error_type: nil)`).
|
|
269
|
+
- **`agent.session.remove(id:)`** — Removes a message; cascades to its `Tool` children when the target carries `tool_calls`. Raises if called on a `Tool` message (use `update(tool_call_id:, ...)` to rewrite a tool result instead).
|
|
268
270
|
|
|
269
271
|
Bulk filling of orphan `tool_use` blocks is handled by `Riffer.config.experimental_history_healing` (see "Healing pending tool results on interrupt" above) — there is no public synthesizer hook.
|
|
270
272
|
|
|
271
|
-
|
|
273
|
+
Lookup patterns that pair with the mutators (via `Enumerable`):
|
|
272
274
|
|
|
273
275
|
```ruby
|
|
274
|
-
agent.
|
|
275
|
-
agent.
|
|
276
|
-
agent.
|
|
277
|
-
agent.orphaned_tool_call_ids
|
|
276
|
+
agent.session.find { |m| m.id == id } # message by id
|
|
277
|
+
agent.session.reverse_each.find { |m| m.is_a?(Riffer::Messages::Tool) && m.tool_call_id == call_id }
|
|
278
|
+
agent.session.reverse_each.find { |m| m.is_a?(Riffer::Messages::Assistant) } # last assistant
|
|
279
|
+
agent.session.orphaned_tool_call_ids # Array[String], zero-cost validation
|
|
278
280
|
```
|
|
279
281
|
|
|
280
282
|
Mutating history while a `stream` enumerator is being consumed is undefined; mutators are intended for use between turns.
|
|
281
283
|
|
|
282
284
|
Mutators do **not** fire `on_message` — that callback is reserved for messages produced by inference (LLM responses, tool execution results). Healing placeholders bypass `on_message` for the same reason; consumers learn that healing happened via `Response#healed_tool_call_ids` (and `StreamEvents::Interrupt#healed_tool_call_ids`).
|
|
283
285
|
|
|
284
|
-
###
|
|
286
|
+
### context
|
|
287
|
+
|
|
288
|
+
The mutable runtime context. A `Hash` threaded into every Proc-based DSL setting, guardrail, tool runtime, and skills resolution, and shared with every `Riffer::Agent::Run` this agent executes. Carries:
|
|
285
289
|
|
|
286
|
-
|
|
290
|
+
- `context[:skills]` — the resolved `Riffer::Skills::Context` when skills are configured.
|
|
291
|
+
- `context[:token_usage]` — the cumulative `Riffer::Providers::TokenUsage`, mutated by each Run as the loop progresses.
|
|
292
|
+
- any caller-provided keys passed via `Agent.new(context: ...)`.
|
|
287
293
|
|
|
288
294
|
```ruby
|
|
289
295
|
agent = MyAgent.new
|
|
290
296
|
agent.generate("Hello!")
|
|
291
297
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
puts "Input: #{agent.token_usage.input_tokens}"
|
|
295
|
-
puts "Output: #{agent.token_usage.output_tokens}"
|
|
296
|
-
end
|
|
298
|
+
agent.context[:token_usage] # cumulative TokenUsage across all calls
|
|
299
|
+
agent.context[:skills] # the Skills::Context, if skills configured
|
|
297
300
|
```
|
|
298
301
|
|
|
299
|
-
Returns `nil` if the provider doesn't report usage, or a `Riffer::TokenUsage` object with accumulated totals.
|
|
300
|
-
|
|
301
302
|
## Response Attributes
|
|
302
303
|
|
|
303
304
|
`Riffer::Agent::Response` is returned by `generate`:
|
|
@@ -333,7 +334,7 @@ The assistant message in the message history stores the parsed hash, so you can
|
|
|
333
334
|
agent = SentimentAgent.new
|
|
334
335
|
agent.generate('Analyze: "I love this!"')
|
|
335
336
|
|
|
336
|
-
msg = agent.messages.last
|
|
337
|
+
msg = agent.session.messages.last
|
|
337
338
|
msg.structured_output? # => true
|
|
338
339
|
msg.structured_output # => {sentiment: "positive", score: 0.95}
|
|
339
340
|
```
|
data/docs/05_AGENT_LOOP.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
When an agent receives a response with tool calls:
|
|
6
6
|
|
|
7
7
|
1. Agent detects `tool_calls` in the assistant message
|
|
8
|
-
2. The configured tool runtime executes the tool calls (sequentially by default, or concurrently with `Riffer::
|
|
8
|
+
2. The configured tool runtime executes the tool calls (sequentially by default, or concurrently with `Riffer::Tools::Runtime::Threaded`):
|
|
9
9
|
- Finds the matching tool class
|
|
10
10
|
- Validates arguments against the tool's parameter schema
|
|
11
11
|
- Calls the tool's `call` method with `context` and arguments
|
|
@@ -58,7 +58,7 @@ Callbacks registered with `on_message` can call `agent.interrupt!` (or `throw :r
|
|
|
58
58
|
|
|
59
59
|
```ruby
|
|
60
60
|
agent = MyAgent.new
|
|
61
|
-
agent.on_message do |msg|
|
|
61
|
+
agent.session.on_message do |msg|
|
|
62
62
|
agent.interrupt!("approval needed") if requires_approval?(msg)
|
|
63
63
|
end
|
|
64
64
|
|
data/docs/06_TOOLS.md
CHANGED
|
@@ -108,12 +108,12 @@ Options:
|
|
|
108
108
|
| `String` | `string` |
|
|
109
109
|
| `Integer` | `integer` |
|
|
110
110
|
| `Float` | `number` |
|
|
111
|
-
| `Riffer::Boolean` | `boolean` |
|
|
111
|
+
| `Riffer::Params::Boolean` | `boolean` |
|
|
112
112
|
| `TrueClass` / `FalseClass` | `boolean` |
|
|
113
113
|
| `Array` | `array` |
|
|
114
114
|
| `Hash` | `object` |
|
|
115
115
|
|
|
116
|
-
`Riffer::Boolean` is the preferred way to declare boolean parameters. `TrueClass` and `FalseClass` continue to work for backwards compatibility.
|
|
116
|
+
`Riffer::Params::Boolean` is the preferred way to declare boolean parameters. `TrueClass` and `FalseClass` continue to work for backwards compatibility.
|
|
117
117
|
|
|
118
118
|
### Nested Parameters
|
|
119
119
|
|
|
@@ -158,7 +158,7 @@ end
|
|
|
158
158
|
|
|
159
159
|
### Accessing Context
|
|
160
160
|
|
|
161
|
-
The `context` argument
|
|
161
|
+
The `context` argument is a `Riffer::Agent::Context` — a typed value object wrapping the Hash passed as `context:` to `Agent.new`. Caller-provided keys are read with `context[:key]` or `context&.dig(:key)`:
|
|
162
162
|
|
|
163
163
|
```ruby
|
|
164
164
|
class UserOrdersTool < Riffer::Tool
|
|
@@ -176,9 +176,14 @@ class UserOrdersTool < Riffer::Tool
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
# Usage
|
|
179
|
-
|
|
179
|
+
MyAgent.new(context: {user_id: 123}).generate("Show my orders")
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
Two keys are framework-managed and exposed as typed accessors:
|
|
183
|
+
|
|
184
|
+
- `context.skills` — the resolved `Riffer::Skills::Context` when the agent has skills configured, otherwise `nil`.
|
|
185
|
+
- `context.token_usage` — the cumulative `Riffer::Providers::TokenUsage` across every run on the agent, or `nil` before the first response.
|
|
186
|
+
|
|
182
187
|
## Response Objects
|
|
183
188
|
|
|
184
189
|
All tools must return a `Riffer::Tools::Response` object from their `call` method. Riffer::Tool provides shorthand methods for creating responses.
|
data/docs/07_TOOL_ADVANCED.md
CHANGED
|
@@ -103,15 +103,15 @@ The LLM receives the error message and can decide how to respond (retry, apologi
|
|
|
103
103
|
|
|
104
104
|
> **Warning:** This feature is experimental and may be removed or changed without warning in a future release.
|
|
105
105
|
|
|
106
|
-
By default, tool calls are executed sequentially in the current thread using `Riffer::
|
|
106
|
+
By default, tool calls are executed sequentially in the current thread using `Riffer::Tools::Runtime::Inline`. You can change how tool calls are executed by configuring a different tool runtime.
|
|
107
107
|
|
|
108
108
|
### Built-in Runtimes
|
|
109
109
|
|
|
110
110
|
| Runtime | Description |
|
|
111
111
|
| ------------------------------- | ---------------------------------------------- |
|
|
112
|
-
| `Riffer::
|
|
113
|
-
| `Riffer::
|
|
114
|
-
| `Riffer::
|
|
112
|
+
| `Riffer::Tools::Runtime::Inline` | Executes tool calls sequentially (default) |
|
|
113
|
+
| `Riffer::Tools::Runtime::Threaded` | Executes tool calls concurrently using threads |
|
|
114
|
+
| `Riffer::Tools::Runtime::Fibers` | Executes tool calls concurrently using fibers |
|
|
115
115
|
|
|
116
116
|
### Per-Agent Configuration
|
|
117
117
|
|
|
@@ -121,14 +121,14 @@ Use the `tool_runtime` class method on your agent:
|
|
|
121
121
|
class MyAgent < Riffer::Agent
|
|
122
122
|
model 'openai/gpt-5-mini'
|
|
123
123
|
uses_tools [WeatherTool, SearchTool]
|
|
124
|
-
tool_runtime Riffer::
|
|
124
|
+
tool_runtime Riffer::Tools::Runtime::Threaded
|
|
125
125
|
end
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
Accepted values:
|
|
129
129
|
|
|
130
|
-
- A `Riffer::
|
|
131
|
-
- A `Riffer::
|
|
130
|
+
- A `Riffer::Tools::Runtime` subclass — instantiated automatically (e.g., `Riffer::Tools::Runtime::Inline`, `Riffer::Tools::Runtime::Threaded`)
|
|
131
|
+
- A `Riffer::Tools::Runtime` instance — for custom runtimes with specific options
|
|
132
132
|
- A `Proc` — evaluated at runtime (see below)
|
|
133
133
|
|
|
134
134
|
### Dynamic Resolution
|
|
@@ -141,11 +141,11 @@ class MyAgent < Riffer::Agent
|
|
|
141
141
|
uses_tools [WeatherTool, SearchTool]
|
|
142
142
|
|
|
143
143
|
tool_runtime ->(context) {
|
|
144
|
-
context&.dig(:parallel) ? Riffer::
|
|
144
|
+
context&.dig(:parallel) ? Riffer::Tools::Runtime::Threaded.new : Riffer::Tools::Runtime::Inline.new
|
|
145
145
|
}
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
MyAgent.new(context: {parallel: true}).generate("Do work")
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
When the lambda accepts a parameter, it receives the `context`. Zero-arity lambdas are also supported.
|
|
@@ -156,7 +156,7 @@ Set a default tool runtime for all agents:
|
|
|
156
156
|
|
|
157
157
|
```ruby
|
|
158
158
|
Riffer.configure do |config|
|
|
159
|
-
config.tool_runtime = Riffer::
|
|
159
|
+
config.tool_runtime = Riffer::Tools::Runtime::Threaded
|
|
160
160
|
end
|
|
161
161
|
```
|
|
162
162
|
|
|
@@ -164,7 +164,7 @@ Per-agent configuration overrides the global default.
|
|
|
164
164
|
|
|
165
165
|
### Threaded Runtime Considerations
|
|
166
166
|
|
|
167
|
-
When using `Riffer::
|
|
167
|
+
When using `Riffer::Tools::Runtime::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.
|
|
168
168
|
|
|
169
169
|
### Threaded Runtime Options
|
|
170
170
|
|
|
@@ -174,7 +174,7 @@ The threaded runtime accepts a `max_concurrency` option (default: 5):
|
|
|
174
174
|
class MyAgent < Riffer::Agent
|
|
175
175
|
model 'openai/gpt-5-mini'
|
|
176
176
|
uses_tools [WeatherTool, SearchTool]
|
|
177
|
-
tool_runtime Riffer::
|
|
177
|
+
tool_runtime Riffer::Tools::Runtime::Threaded.new(max_concurrency: 3)
|
|
178
178
|
end
|
|
179
179
|
```
|
|
180
180
|
|
|
@@ -191,7 +191,7 @@ gem "async"
|
|
|
191
191
|
class MyAgent < Riffer::Agent
|
|
192
192
|
model 'openai/gpt-5-mini'
|
|
193
193
|
uses_tools [WeatherTool, SearchTool]
|
|
194
|
-
tool_runtime Riffer::
|
|
194
|
+
tool_runtime Riffer::Tools::Runtime::Fibers
|
|
195
195
|
end
|
|
196
196
|
```
|
|
197
197
|
|
|
@@ -201,7 +201,7 @@ By default, all tool calls run as fibers without a concurrency limit. You can op
|
|
|
201
201
|
class MyAgent < Riffer::Agent
|
|
202
202
|
model 'openai/gpt-5-mini'
|
|
203
203
|
uses_tools [WeatherTool, SearchTool]
|
|
204
|
-
tool_runtime Riffer::
|
|
204
|
+
tool_runtime Riffer::Tools::Runtime::Fibers.new(max_concurrency: 10)
|
|
205
205
|
end
|
|
206
206
|
```
|
|
207
207
|
|
|
@@ -209,10 +209,10 @@ Fibers use cooperative scheduling — they yield control at I/O boundaries (netw
|
|
|
209
209
|
|
|
210
210
|
### Custom Runtimes
|
|
211
211
|
|
|
212
|
-
Create a custom runtime by subclassing `Riffer::
|
|
212
|
+
Create a custom runtime by subclassing `Riffer::Tools::Runtime` and overriding the private `dispatch_tool_call` method:
|
|
213
213
|
|
|
214
214
|
```ruby
|
|
215
|
-
class HttpToolRuntime < Riffer::
|
|
215
|
+
class HttpToolRuntime < Riffer::Tools::Runtime
|
|
216
216
|
private
|
|
217
217
|
|
|
218
218
|
def dispatch_tool_call(tool_call, tools:, context:, assistant_message: nil)
|
|
@@ -235,7 +235,7 @@ end
|
|
|
235
235
|
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:
|
|
236
236
|
|
|
237
237
|
```ruby
|
|
238
|
-
class InstrumentedRuntime < Riffer::
|
|
238
|
+
class InstrumentedRuntime < Riffer::Tools::Runtime::Inline
|
|
239
239
|
private
|
|
240
240
|
|
|
241
241
|
def around_tool_call(tool_call, context:, assistant_message: nil)
|