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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +18 -11
  3. data/.agents/code-style.md +1 -1
  4. data/.agents/rbs-inline.md +2 -2
  5. data/.agents/testing.md +9 -5
  6. data/.release-please-manifest.json +1 -1
  7. data/AGENTS.md +17 -10
  8. data/CHANGELOG.md +19 -0
  9. data/README.md +17 -18
  10. data/Steepfile +7 -1
  11. data/docs/03_AGENTS.md +34 -3
  12. data/docs/04_AGENT_LIFECYCLE.md +87 -86
  13. data/docs/05_AGENT_LOOP.md +2 -2
  14. data/docs/06_TOOLS.md +9 -4
  15. data/docs/07_TOOL_ADVANCED.md +17 -17
  16. data/docs/08_MESSAGES.md +25 -32
  17. data/docs/09_STREAM_EVENTS.md +1 -1
  18. data/docs/10_CONFIGURATION.md +7 -18
  19. data/docs/providers/01_PROVIDERS.md +6 -0
  20. data/docs/providers/06_MOCK_PROVIDER.md +2 -1
  21. data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
  22. data/docs/providers/08_GEMINI.md +2 -2
  23. data/docs/providers/09_OPENROUTER.md +242 -0
  24. data/lib/riffer/agent/config.rb +173 -0
  25. data/lib/riffer/agent/context.rb +125 -0
  26. data/lib/riffer/agent/run.rb +308 -0
  27. data/lib/riffer/agent/session/repair.rb +112 -0
  28. data/lib/riffer/agent/session.rb +268 -0
  29. data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
  30. data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
  31. data/lib/riffer/agent.rb +234 -923
  32. data/lib/riffer/config.rb +14 -7
  33. data/lib/riffer/evals/evaluator.rb +13 -3
  34. data/lib/riffer/evals/judge.rb +2 -2
  35. data/lib/riffer/evals/run_result.rb +2 -1
  36. data/lib/riffer/evals/scenario_result.rb +2 -1
  37. data/lib/riffer/guardrails/runner.rb +3 -2
  38. data/lib/riffer/helpers/call_or_value.rb +16 -0
  39. data/lib/riffer/helpers.rb +0 -1
  40. data/lib/riffer/mcp/authenticated_tool.rb +4 -0
  41. data/lib/riffer/mcp/client.rb +1 -1
  42. data/lib/riffer/mcp/registration.rb +2 -3
  43. data/lib/riffer/mcp/registry.rb +3 -1
  44. data/lib/riffer/mcp/tool_factory.rb +5 -0
  45. data/lib/riffer/messages/assistant.rb +9 -3
  46. data/lib/riffer/messages/base.rb +22 -0
  47. data/lib/riffer/messages/converter.rb +6 -6
  48. data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
  49. data/lib/riffer/messages/tool.rb +1 -1
  50. data/lib/riffer/messages/user.rb +4 -4
  51. data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
  52. data/lib/riffer/{param.rb → params/param.rb} +6 -6
  53. data/lib/riffer/params.rb +27 -21
  54. data/lib/riffer/providers/amazon_bedrock.rb +19 -20
  55. data/lib/riffer/providers/anthropic.rb +27 -28
  56. data/lib/riffer/providers/base.rb +10 -9
  57. data/lib/riffer/providers/gemini.rb +15 -12
  58. data/lib/riffer/providers/mock.rb +41 -13
  59. data/lib/riffer/providers/open_ai.rb +24 -22
  60. data/lib/riffer/providers/open_router.rb +318 -0
  61. data/lib/riffer/providers/repository.rb +1 -0
  62. data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
  63. data/lib/riffer/providers.rb +1 -0
  64. data/lib/riffer/runner/fibers.rb +4 -3
  65. data/lib/riffer/runner/sequential.rb +1 -1
  66. data/lib/riffer/runner/threaded.rb +1 -1
  67. data/lib/riffer/runner.rb +1 -1
  68. data/lib/riffer/skills/activate_tool.rb +4 -3
  69. data/lib/riffer/skills/config.rb +1 -1
  70. data/lib/riffer/skills/context.rb +3 -3
  71. data/lib/riffer/skills/filesystem_backend.rb +7 -5
  72. data/lib/riffer/skills/markdown_adapter.rb +1 -1
  73. data/lib/riffer/skills/xml_adapter.rb +1 -1
  74. data/lib/riffer/stream_events/interrupt.rb +1 -1
  75. data/lib/riffer/stream_events/token_usage_done.rb +2 -2
  76. data/lib/riffer/stream_events/web_search_status.rb +1 -1
  77. data/lib/riffer/tool.rb +3 -3
  78. data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
  79. data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
  80. data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
  81. data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +9 -9
  82. data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
  83. data/lib/riffer/version.rb +1 -1
  84. data/lib/riffer.rb +2 -1
  85. data/sig/generated/riffer/agent/config.rbs +119 -0
  86. data/sig/generated/riffer/agent/context.rbs +91 -0
  87. data/sig/generated/riffer/agent/run.rbs +144 -0
  88. data/sig/generated/riffer/agent/session/repair.rbs +51 -0
  89. data/sig/generated/riffer/agent/session.rbs +145 -0
  90. data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
  91. data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
  92. data/sig/generated/riffer/agent.rbs +143 -342
  93. data/sig/generated/riffer/config.rbs +17 -5
  94. data/sig/generated/riffer/evals/judge.rbs +2 -2
  95. data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
  96. data/sig/generated/riffer/helpers.rbs +0 -1
  97. data/sig/generated/riffer/messages/assistant.rbs +7 -3
  98. data/sig/generated/riffer/messages/base.rbs +18 -0
  99. data/sig/generated/riffer/messages/converter.rbs +4 -4
  100. data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
  101. data/sig/generated/riffer/messages/user.rbs +4 -4
  102. data/sig/generated/riffer/params/boolean.rbs +10 -0
  103. data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
  104. data/sig/generated/riffer/params.rbs +15 -15
  105. data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
  106. data/sig/generated/riffer/providers/anthropic.rbs +4 -4
  107. data/sig/generated/riffer/providers/base.rbs +10 -10
  108. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  109. data/sig/generated/riffer/providers/mock.rbs +25 -5
  110. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  111. data/sig/generated/riffer/providers/open_router.rbs +85 -0
  112. data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
  113. data/sig/generated/riffer/providers.rbs +1 -0
  114. data/sig/generated/riffer/runner/fibers.rbs +2 -2
  115. data/sig/generated/riffer/runner/sequential.rbs +2 -2
  116. data/sig/generated/riffer/runner/threaded.rbs +2 -2
  117. data/sig/generated/riffer/runner.rbs +2 -2
  118. data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
  119. data/sig/generated/riffer/skills/config.rbs +1 -1
  120. data/sig/generated/riffer/skills/context.rbs +2 -2
  121. data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
  122. data/sig/generated/riffer/tool.rbs +5 -5
  123. data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
  124. data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
  125. data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
  126. data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +12 -12
  127. data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
  128. data/sig/stubs/agent_ivars.rbs +7 -0
  129. data/sig/stubs/async.rbs +24 -0
  130. data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
  131. data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
  132. data/sig/stubs/extend_self.rbs +11 -0
  133. data/sig/stubs/lib_ivars.rbs +101 -0
  134. data/sig/stubs/mcp_sdk.rbs +22 -0
  135. data/sig/stubs/provider_ivars.rbs +36 -0
  136. data/sig/stubs/provider_sdk_methods.rbs +50 -0
  137. data/sig/stubs/zeitwerk.rbs +12 -0
  138. metadata +54 -33
  139. data/lib/riffer/core.rb +0 -28
  140. data/lib/riffer/helpers/validations.rb +0 -18
  141. data/sig/generated/riffer/boolean.rbs +0 -10
  142. data/sig/generated/riffer/core.rbs +0 -19
  143. data/sig/generated/riffer/helpers/validations.rbs +0 -12
@@ -1,69 +1,64 @@
1
1
  # Agent Lifecycle
2
2
 
3
- ## Instance Methods
3
+ ## Construction
4
4
 
5
- ### generate
5
+ ### Agent.new
6
6
 
7
- Generates a response synchronously. Returns a `Riffer::Agent::Response` object.
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
- The behavior depends on what you pass and the agent's current state:
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
- | Input | Agent state | Behavior |
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
- **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`.
20
+ Generates a response synchronously. Returns a `Riffer::Agent::Response` object.
19
21
 
20
22
  ```ruby
21
- agent.generate('Hello', context: {user_id: 123})
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
- # Restore from persisted messages (cross-process resume)
45
- agent = MyAgent.new
46
- response = agent.generate(persisted_messages, context: {user_id: 123})
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 (string prompt + files shorthand)
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. Follows the same input rules as `generate` — a string starts a new conversation or continues an existing one, an array restores from persisted data.
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
- ### messages
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
- .generate('Hello')
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 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.
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
- 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.
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 messages via on_message callback
201
+ # During generation, persist each new message via on_message
199
202
  # Later, in a new process:
200
- agent = MyAgent.new
201
- response = agent.generate(persisted_messages, context: {user_id: 123})
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(persisted_messages).each do |event|
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
- **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.
211
-
212
- ### Building System Messages for Persistence
214
+ ### Reading System Messages for Persistence
213
215
 
214
- 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.
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 methods return a `Riffer::Messages::System` or `nil` (when unconfigured). They accept an optional `context:` keyword, just like `generate`.
218
+ Both return `Riffer::Messages::System` or `nil` (when unconfigured / empty).
217
219
 
218
220
  ```ruby
219
- agent = MyAgent.new
220
- sys = agent.generate_instruction_message(context: ctx) # => Riffer::Messages::System or nil
221
- skills = agent.generate_skills_message(context: ctx) # => Riffer::Messages::System or nil
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
- messages = [sys, skills, user_msg].compact
225
- MyAgent.new.generate(messages, context: ctx)
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 `replace_tool_result` mutator below to upgrade a placeholder after the interrupt returns.
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 agent exposes a small set of in-place mutators that enforce the `tool_use` ↔ `tool_result` invariant on every operation. Use these to align the agent's history with external state (persisted transcript, partial output that wasn't actually delivered, etc.) without rebuilding the agent.
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.replace_assistant_content(id:, content:)`** — In-place truncation/edit. Preserves `tool_calls`, `token_usage`, and `id`. Empty `content` delegates to `remove_message`.
266
- - **`agent.remove_message(id:)`** — Removes a message; cascades to its `Tool` children when the target carries `tool_calls`. Raises if called on a `Tool` (use `replace_tool_result`).
267
- - **`agent.replace_tool_result(tool_call_id:, content:, error:, error_type:)`** — Replace a tool result in place, preserving `name` and `id`. Use this to upgrade an interrupt-time placeholder once the real result is available.
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
- Read accessors that pair with the mutators:
273
+ Lookup patterns that pair with the mutators (via `Enumerable`):
272
274
 
273
275
  ```ruby
274
- agent.message_by_id(id) # => Riffer::Messages::Base or nil
275
- agent.tool_message_for(call_id) # => Riffer::Messages::Tool or nil
276
- agent.last_assistant # => Riffer::Messages::Assistant or nil
277
- agent.orphaned_tool_call_ids # => Array[String] (zero-cost validation)
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
- ### token_usage
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
- Access cumulative token usage across all LLM calls:
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
- if agent.token_usage
293
- puts "Total tokens: #{agent.token_usage.total_tokens}"
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
  ```
@@ -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::ToolRuntime::Threaded`):
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 receives whatever was passed as `context:` to `generate`:
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
- agent.generate("Show my orders", context: {user_id: 123})
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.
@@ -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::ToolRuntime::Inline`. You can change how tool calls are executed by configuring a different tool runtime.
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::ToolRuntime::Inline` | Executes tool calls sequentially (default) |
113
- | `Riffer::ToolRuntime::Threaded` | Executes tool calls concurrently using threads |
114
- | `Riffer::ToolRuntime::Fibers` | Executes tool calls concurrently using fibers |
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::ToolRuntime::Threaded
124
+ tool_runtime Riffer::Tools::Runtime::Threaded
125
125
  end
126
126
  ```
127
127
 
128
128
  Accepted values:
129
129
 
130
- - A `Riffer::ToolRuntime` subclass — instantiated automatically (e.g., `Riffer::ToolRuntime::Inline`, `Riffer::ToolRuntime::Threaded`)
131
- - A `Riffer::ToolRuntime` instance — for custom runtimes with specific options
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::ToolRuntime::Threaded.new : Riffer::ToolRuntime::Inline.new
144
+ context&.dig(:parallel) ? Riffer::Tools::Runtime::Threaded.new : Riffer::Tools::Runtime::Inline.new
145
145
  }
146
146
  end
147
147
 
148
- agent.generate("Do work", context: {parallel: true})
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::ToolRuntime::Threaded
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::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.
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::ToolRuntime::Threaded.new(max_concurrency: 3)
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::ToolRuntime::Fibers
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::ToolRuntime::Fibers.new(max_concurrency: 10)
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::ToolRuntime` and overriding the private `dispatch_tool_call` method:
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::ToolRuntime
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::ToolRuntime::Inline
238
+ class InstrumentedRuntime < Riffer::Tools::Runtime::Inline
239
239
  private
240
240
 
241
241
  def around_tool_call(tool_call, context:, assistant_message: nil)