riffer 0.7.0 → 0.9.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +113 -0
  3. data/.agents/code-style.md +42 -0
  4. data/.agents/providers.md +46 -0
  5. data/.agents/rdoc.md +51 -0
  6. data/.agents/testing.md +56 -0
  7. data/.release-please-manifest.json +1 -1
  8. data/AGENTS.md +21 -308
  9. data/CHANGELOG.md +17 -0
  10. data/README.md +21 -112
  11. data/Rakefile +1 -1
  12. data/docs/01_OVERVIEW.md +106 -0
  13. data/docs/02_GETTING_STARTED.md +128 -0
  14. data/docs/03_AGENTS.md +226 -0
  15. data/docs/04_TOOLS.md +342 -0
  16. data/docs/05_MESSAGES.md +173 -0
  17. data/docs/06_STREAM_EVENTS.md +191 -0
  18. data/docs/07_CONFIGURATION.md +195 -0
  19. data/docs_providers/01_PROVIDERS.md +168 -0
  20. data/docs_providers/02_AMAZON_BEDROCK.md +196 -0
  21. data/docs_providers/03_ANTHROPIC.md +211 -0
  22. data/docs_providers/04_OPENAI.md +157 -0
  23. data/docs_providers/05_TEST_PROVIDER.md +163 -0
  24. data/docs_providers/06_CUSTOM_PROVIDERS.md +304 -0
  25. data/lib/riffer/agent.rb +103 -63
  26. data/lib/riffer/config.rb +20 -12
  27. data/lib/riffer/core.rb +7 -7
  28. data/lib/riffer/helpers/class_name_converter.rb +6 -3
  29. data/lib/riffer/helpers/dependencies.rb +18 -0
  30. data/lib/riffer/helpers/validations.rb +9 -0
  31. data/lib/riffer/messages/assistant.rb +23 -1
  32. data/lib/riffer/messages/base.rb +15 -0
  33. data/lib/riffer/messages/converter.rb +15 -5
  34. data/lib/riffer/messages/system.rb +8 -1
  35. data/lib/riffer/messages/tool.rb +45 -2
  36. data/lib/riffer/messages/user.rb +8 -1
  37. data/lib/riffer/messages.rb +7 -0
  38. data/lib/riffer/providers/amazon_bedrock.rb +8 -4
  39. data/lib/riffer/providers/anthropic.rb +209 -0
  40. data/lib/riffer/providers/base.rb +17 -12
  41. data/lib/riffer/providers/open_ai.rb +7 -1
  42. data/lib/riffer/providers/repository.rb +9 -4
  43. data/lib/riffer/providers/test.rb +25 -7
  44. data/lib/riffer/providers.rb +6 -0
  45. data/lib/riffer/stream_events/base.rb +13 -1
  46. data/lib/riffer/stream_events/reasoning_delta.rb +15 -1
  47. data/lib/riffer/stream_events/reasoning_done.rb +15 -1
  48. data/lib/riffer/stream_events/text_delta.rb +14 -1
  49. data/lib/riffer/stream_events/text_done.rb +14 -1
  50. data/lib/riffer/stream_events/tool_call_delta.rb +18 -11
  51. data/lib/riffer/stream_events/tool_call_done.rb +22 -12
  52. data/lib/riffer/stream_events.rb +9 -0
  53. data/lib/riffer/tool.rb +92 -25
  54. data/lib/riffer/tools/param.rb +19 -16
  55. data/lib/riffer/tools/params.rb +28 -22
  56. data/lib/riffer/tools/response.rb +90 -0
  57. data/lib/riffer/tools.rb +6 -0
  58. data/lib/riffer/version.rb +1 -1
  59. data/lib/riffer.rb +21 -21
  60. metadata +35 -1
data/docs/04_TOOLS.md ADDED
@@ -0,0 +1,342 @@
1
+ # Tools
2
+
3
+ Tools are callable functions that agents can invoke to interact with external systems, fetch data, or perform actions.
4
+
5
+ ## Defining a Tool
6
+
7
+ Create a tool by subclassing `Riffer::Tool`:
8
+
9
+ ```ruby
10
+ class WeatherTool < Riffer::Tool
11
+ description "Gets the current weather for a city"
12
+
13
+ params do
14
+ required :city, String, description: "The city name"
15
+ optional :units, String, default: "celsius", enum: ["celsius", "fahrenheit"]
16
+ end
17
+
18
+ def call(context:, city:, units: nil)
19
+ weather = WeatherAPI.fetch(city, units: units || "celsius")
20
+ text("The weather in #{city} is #{weather.temperature} #{units}.")
21
+ end
22
+ end
23
+ ```
24
+
25
+ ## Configuration Methods
26
+
27
+ ### description
28
+
29
+ Sets a description that helps the LLM understand when to use the tool:
30
+
31
+ ```ruby
32
+ class SearchTool < Riffer::Tool
33
+ description "Searches the knowledge base for relevant information"
34
+ end
35
+ ```
36
+
37
+ ### identifier / name
38
+
39
+ Sets a custom identifier (defaults to snake_case class name):
40
+
41
+ ```ruby
42
+ class SearchTool < Riffer::Tool
43
+ identifier 'kb_search'
44
+ end
45
+
46
+ SearchTool.identifier # => "kb_search"
47
+ SearchTool.name # => "kb_search" (alias)
48
+ ```
49
+
50
+ ### params
51
+
52
+ Defines the tool's parameters using a DSL:
53
+
54
+ ```ruby
55
+ class CreateOrderTool < Riffer::Tool
56
+ params do
57
+ required :product_id, Integer, description: "The product ID"
58
+ required :quantity, Integer, description: "Number of items"
59
+ optional :notes, String, description: "Order notes"
60
+ optional :priority, String, default: "normal", enum: ["low", "normal", "high"]
61
+ end
62
+ end
63
+ ```
64
+
65
+ ## Parameter DSL
66
+
67
+ ### required
68
+
69
+ Defines a required parameter:
70
+
71
+ ```ruby
72
+ params do
73
+ required :name, String, description: "The user's name"
74
+ required :age, Integer, description: "The user's age"
75
+ end
76
+ ```
77
+
78
+ Options:
79
+
80
+ - `description` - Human-readable description for the LLM
81
+ - `enum` - Array of allowed values
82
+
83
+ ### optional
84
+
85
+ Defines an optional parameter:
86
+
87
+ ```ruby
88
+ params do
89
+ optional :limit, Integer, default: 10, description: "Max results"
90
+ optional :format, String, enum: ["json", "xml"], description: "Output format"
91
+ end
92
+ ```
93
+
94
+ Options:
95
+
96
+ - `description` - Human-readable description
97
+ - `default` - Default value when not provided
98
+ - `enum` - Array of allowed values
99
+
100
+ ### Supported Types
101
+
102
+ | Ruby Type | JSON Schema Type |
103
+ | -------------------------- | ---------------- |
104
+ | `String` | `string` |
105
+ | `Integer` | `integer` |
106
+ | `Float` | `number` |
107
+ | `TrueClass` / `FalseClass` | `boolean` |
108
+ | `Array` | `array` |
109
+ | `Hash` | `object` |
110
+
111
+ ## The call Method
112
+
113
+ Every tool must implement the `call` method and return a `Riffer::Tools::Response`:
114
+
115
+ ```ruby
116
+ def call(context:, **kwargs)
117
+ # context - The tool_context passed to agent.generate()
118
+ # kwargs - Validated parameters
119
+ #
120
+ # Must return a Riffer::Tools::Response
121
+ end
122
+ ```
123
+
124
+ ### Accessing Context
125
+
126
+ The `context` argument receives whatever was passed to `tool_context`:
127
+
128
+ ```ruby
129
+ class UserOrdersTool < Riffer::Tool
130
+ description "Gets the current user's orders"
131
+
132
+ def call(context:)
133
+ user_id = context&.dig(:user_id)
134
+ unless user_id
135
+ return error("No user ID provided")
136
+ end
137
+
138
+ orders = Order.where(user_id: user_id)
139
+ text(orders.map(&:to_s).join("\n"))
140
+ end
141
+ end
142
+
143
+ # Usage
144
+ agent.generate("Show my orders", tool_context: {user_id: 123})
145
+ ```
146
+
147
+ ## Response Objects
148
+
149
+ All tools must return a `Riffer::Tools::Response` object from their `call` method. Riffer::Tool provides shorthand methods for creating responses.
150
+
151
+ ### Success Responses
152
+
153
+ Use `text` for string responses and `json` for structured data:
154
+
155
+ ```ruby
156
+ def call(context:, query:)
157
+ results = Database.search(query)
158
+
159
+ if results.empty?
160
+ text("No results found for '#{query}'")
161
+ else
162
+ text(results.map { |r| "- #{r.title}: #{r.summary}" }.join("\n"))
163
+ end
164
+ end
165
+ ```
166
+
167
+ #### text
168
+
169
+ Converts the result to a string via `to_s`:
170
+
171
+ ```ruby
172
+ text("Hello, world!")
173
+ # => content: "Hello, world!"
174
+
175
+ text(42)
176
+ # => content: "42"
177
+ ```
178
+
179
+ #### json
180
+
181
+ Converts the result to JSON via `to_json`:
182
+
183
+ ```ruby
184
+ json({name: "Alice", age: 30})
185
+ # => content: '{"name":"Alice","age":30}'
186
+
187
+ json([1, 2, 3])
188
+ # => content: '[1,2,3]'
189
+ ```
190
+
191
+ ### Error Responses
192
+
193
+ Use `error(message, type:)` for errors:
194
+
195
+ ```ruby
196
+ def call(context:, user_id:)
197
+ user = User.find_by(id: user_id)
198
+
199
+ unless user
200
+ return error("User not found", type: :not_found)
201
+ end
202
+
203
+ text("User: #{user.name}")
204
+ end
205
+ ```
206
+
207
+ The error type is any symbol that describes the error category:
208
+
209
+ ```ruby
210
+ error("Invalid input", type: :validation_error)
211
+ error("Service unavailable", type: :service_error)
212
+ error("Rate limit exceeded", type: :rate_limit)
213
+ ```
214
+
215
+ If no type is specified, it defaults to `:execution_error`.
216
+
217
+ ### Using Riffer::Tools::Response Directly
218
+
219
+ The shorthand methods delegate to `Riffer::Tools::Response`. You can also use the class directly if preferred:
220
+
221
+ ```ruby
222
+ Riffer::Tools::Response.text("Hello")
223
+ Riffer::Tools::Response.json({data: [1, 2, 3]})
224
+ Riffer::Tools::Response.error("Failed", type: :custom_error)
225
+ ```
226
+
227
+ ### Response Methods
228
+
229
+ ```ruby
230
+ response = text("result")
231
+ response.content # => "result"
232
+ response.success? # => true
233
+ response.error? # => false
234
+ response.error_message # => nil
235
+ response.error_type # => nil
236
+
237
+ error_response = error("failed", type: :not_found)
238
+ error_response.content # => "failed"
239
+ error_response.success? # => false
240
+ error_response.error? # => true
241
+ error_response.error_message # => "failed"
242
+ error_response.error_type # => :not_found
243
+ ```
244
+
245
+ ## Timeout Configuration
246
+
247
+ Configure timeouts to prevent tools from running indefinitely. The default timeout is 10 seconds.
248
+
249
+ ```ruby
250
+ class SlowExternalApiTool < Riffer::Tool
251
+ description "Calls a slow external API"
252
+ timeout 30 # 30 seconds
253
+
254
+ def call(context:, query:)
255
+ result = ExternalAPI.search(query)
256
+ text(result)
257
+ end
258
+ end
259
+ ```
260
+
261
+ When a tool times out, the error is reported to the LLM with error type `:timeout_error`, allowing it to respond appropriately (e.g., suggest retrying or using a different approach).
262
+
263
+ ## Validation
264
+
265
+ Arguments are automatically validated before `call` is invoked:
266
+
267
+ - Required parameters must be present
268
+ - Types must match the schema
269
+ - Enum values must be in the allowed list
270
+
271
+ Validation errors are captured and sent back to the LLM as tool results with error type `:validation_error`.
272
+
273
+ ## JSON Schema Generation
274
+
275
+ Riffer automatically generates JSON Schema for each tool:
276
+
277
+ ```ruby
278
+ WeatherTool.parameters_schema
279
+ # => {
280
+ # type: "object",
281
+ # properties: {
282
+ # "city" => {type: "string", description: "The city name"},
283
+ # "units" => {type: "string", enum: ["celsius", "fahrenheit"]}
284
+ # },
285
+ # required: ["city"],
286
+ # additionalProperties: false
287
+ # }
288
+ ```
289
+
290
+ ## Registering Tools with Agents
291
+
292
+ ### Static Registration
293
+
294
+ ```ruby
295
+ class MyAgent < Riffer::Agent
296
+ model 'openai/gpt-4o'
297
+ uses_tools [WeatherTool, SearchTool]
298
+ end
299
+ ```
300
+
301
+ ### Dynamic Registration
302
+
303
+ Use a lambda for context-aware tool resolution:
304
+
305
+ ```ruby
306
+ class MyAgent < Riffer::Agent
307
+ model 'openai/gpt-4o'
308
+
309
+ uses_tools ->(context) {
310
+ tools = [PublicSearchTool]
311
+
312
+ if context&.dig(:user)&.premium?
313
+ tools << PremiumAnalyticsTool
314
+ end
315
+
316
+ if context&.dig(:user)&.admin?
317
+ tools << AdminTool
318
+ end
319
+
320
+ tools
321
+ }
322
+ end
323
+ ```
324
+
325
+ ## Error Handling
326
+
327
+ Errors can be returned explicitly using `error`:
328
+
329
+ ```ruby
330
+ def call(context:, query:)
331
+ results = ExternalAPI.search(query)
332
+ json(results)
333
+ rescue RateLimitError => e
334
+ error("API rate limit exceeded, please try again later", type: :rate_limit)
335
+ rescue => e
336
+ error("Search failed: #{e.message}")
337
+ end
338
+ ```
339
+
340
+ Unhandled exceptions are caught by Riffer and converted to error responses with type `:execution_error`. However, it's recommended to handle expected errors explicitly for better error messages.
341
+
342
+ The LLM receives the error message and can decide how to respond (retry, apologize, ask for different input, etc.).
@@ -0,0 +1,173 @@
1
+ # Messages
2
+
3
+ Messages represent the conversation between users and the assistant. Riffer uses strongly-typed message objects to ensure consistency and type safety.
4
+
5
+ ## Message Types
6
+
7
+ ### System
8
+
9
+ System messages provide instructions to the LLM:
10
+
11
+ ```ruby
12
+ msg = Riffer::Messages::System.new("You are a helpful assistant.")
13
+ msg.role # => :system
14
+ msg.content # => "You are a helpful assistant."
15
+ msg.to_h # => {role: :system, content: "You are a helpful assistant."}
16
+ ```
17
+
18
+ System messages are typically set via agent `instructions` and automatically prepended to conversations.
19
+
20
+ ### User
21
+
22
+ User messages represent input from the user:
23
+
24
+ ```ruby
25
+ msg = Riffer::Messages::User.new("Hello, how are you?")
26
+ msg.role # => :user
27
+ msg.content # => "Hello, how are you?"
28
+ msg.to_h # => {role: :user, content: "Hello, how are you?"}
29
+ ```
30
+
31
+ ### Assistant
32
+
33
+ Assistant messages represent LLM responses, potentially including tool calls:
34
+
35
+ ```ruby
36
+ # Text-only response
37
+ msg = Riffer::Messages::Assistant.new("I'm doing well, thank you!")
38
+ msg.role # => :assistant
39
+ msg.content # => "I'm doing well, thank you!"
40
+ msg.tool_calls # => []
41
+
42
+ # Response with tool calls
43
+ msg = Riffer::Messages::Assistant.new("", tool_calls: [
44
+ {id: "call_123", call_id: "call_123", name: "weather_tool", arguments: '{"city":"Tokyo"}'}
45
+ ])
46
+ msg.tool_calls # => [{id: "call_123", ...}]
47
+ msg.to_h # => {role: "assistant", content: "", tool_calls: [...]}
48
+ ```
49
+
50
+ ### Tool
51
+
52
+ Tool messages contain the results of tool executions:
53
+
54
+ ```ruby
55
+ msg = Riffer::Messages::Tool.new(
56
+ "The weather in Tokyo is 22C and sunny.",
57
+ tool_call_id: "call_123",
58
+ name: "weather_tool"
59
+ )
60
+ msg.role # => :tool
61
+ msg.content # => "The weather in Tokyo is 22C and sunny."
62
+ msg.tool_call_id # => "call_123"
63
+ msg.name # => "weather_tool"
64
+ msg.error? # => false
65
+
66
+ # Error result
67
+ msg = Riffer::Messages::Tool.new(
68
+ "API rate limit exceeded",
69
+ tool_call_id: "call_123",
70
+ name: "weather_tool",
71
+ error: "API rate limit exceeded",
72
+ error_type: :execution_error
73
+ )
74
+ msg.error? # => true
75
+ msg.error # => "API rate limit exceeded"
76
+ msg.error_type # => :execution_error
77
+ ```
78
+
79
+ ## Using Messages with Agents
80
+
81
+ ### String Prompts
82
+
83
+ The simplest way to interact with an agent:
84
+
85
+ ```ruby
86
+ agent = MyAgent.new
87
+ response = agent.generate("Hello!")
88
+ ```
89
+
90
+ This creates a `User` message internally.
91
+
92
+ ### Message Arrays
93
+
94
+ For multi-turn conversations, pass an array of messages:
95
+
96
+ ```ruby
97
+ messages = [
98
+ {role: :user, content: "What's the weather?"},
99
+ {role: :assistant, content: "I'll check that for you."},
100
+ {role: :user, content: "Thanks, I meant in Tokyo specifically."}
101
+ ]
102
+
103
+ response = agent.generate(messages)
104
+ ```
105
+
106
+ Messages can be hashes or `Riffer::Messages::Base` objects:
107
+
108
+ ```ruby
109
+ messages = [
110
+ Riffer::Messages::User.new("Hello"),
111
+ Riffer::Messages::Assistant.new("Hi there!"),
112
+ Riffer::Messages::User.new("How are you?")
113
+ ]
114
+
115
+ response = agent.generate(messages)
116
+ ```
117
+
118
+ ### Accessing Message History
119
+
120
+ After calling `generate` or `stream`, access the full conversation:
121
+
122
+ ```ruby
123
+ agent = MyAgent.new
124
+ agent.generate("Hello!")
125
+
126
+ agent.messages.each do |msg|
127
+ puts "[#{msg.role}] #{msg.content}"
128
+ end
129
+ # [system] You are a helpful assistant.
130
+ # [user] Hello!
131
+ # [assistant] Hi there! How can I help you today?
132
+ ```
133
+
134
+ ## Tool Call Structure
135
+
136
+ Tool calls in assistant messages have this structure:
137
+
138
+ ```ruby
139
+ {
140
+ id: "item_123", # Item identifier
141
+ call_id: "call_456", # Call identifier for response matching
142
+ name: "weather_tool", # Tool name
143
+ arguments: '{"city":"Tokyo"}' # JSON string of arguments
144
+ }
145
+ ```
146
+
147
+ When creating tool result messages, use the `id` as `tool_call_id`.
148
+
149
+ ## Message Emission
150
+
151
+ Agents can emit messages as they're added during generation via the `on_message` callback. This is useful for persistence or real-time logging. Only agent-generated messages (Assistant, Tool) are emitted—not inputs (System, User).
152
+
153
+ See [Agents - on_message](03_AGENTS.md#on_message) for details.
154
+
155
+ ## Base Class
156
+
157
+ All messages inherit from `Riffer::Messages::Base`:
158
+
159
+ ```ruby
160
+ class Riffer::Messages::Base
161
+ attr_reader :content
162
+
163
+ def role
164
+ raise NotImplementedError
165
+ end
166
+
167
+ def to_h
168
+ {role: role, content: content}
169
+ end
170
+ end
171
+ ```
172
+
173
+ Subclasses implement `role` and optionally extend `to_h` with additional fields.
@@ -0,0 +1,191 @@
1
+ # Stream Events
2
+
3
+ When streaming responses, Riffer emits typed events that represent incremental updates from the LLM.
4
+
5
+ ## Using Streaming
6
+
7
+ Use `stream` instead of `generate` to receive events as they arrive:
8
+
9
+ ```ruby
10
+ agent = MyAgent.new
11
+
12
+ agent.stream("Tell me a story").each do |event|
13
+ case event
14
+ when Riffer::StreamEvents::TextDelta
15
+ print event.content
16
+ when Riffer::StreamEvents::TextDone
17
+ puts "\n[Complete]"
18
+ when Riffer::StreamEvents::ToolCallDelta
19
+ # Tool call being built
20
+ when Riffer::StreamEvents::ToolCallDone
21
+ puts "[Tool: #{event.name}]"
22
+ end
23
+ end
24
+ ```
25
+
26
+ ## Event Types
27
+
28
+ ### TextDelta
29
+
30
+ Emitted when incremental text content is received:
31
+
32
+ ```ruby
33
+ event = Riffer::StreamEvents::TextDelta.new("Hello ")
34
+ event.role # => "assistant"
35
+ event.content # => "Hello "
36
+ event.to_h # => {role: "assistant", content: "Hello "}
37
+ ```
38
+
39
+ Use this to display text in real-time as it streams.
40
+
41
+ ### TextDone
42
+
43
+ Emitted when text generation is complete:
44
+
45
+ ```ruby
46
+ event = Riffer::StreamEvents::TextDone.new("Hello, how can I help you?")
47
+ event.role # => "assistant"
48
+ event.content # => "Hello, how can I help you?"
49
+ event.to_h # => {role: "assistant", content: "Hello, how can I help you?"}
50
+ ```
51
+
52
+ Contains the complete final text.
53
+
54
+ ### ToolCallDelta
55
+
56
+ Emitted when tool call arguments are being streamed:
57
+
58
+ ```ruby
59
+ event = Riffer::StreamEvents::ToolCallDelta.new(
60
+ item_id: "item_123",
61
+ name: "weather_tool",
62
+ arguments_delta: '{"city":'
63
+ )
64
+ event.role # => "assistant"
65
+ event.item_id # => "item_123"
66
+ event.name # => "weather_tool"
67
+ event.arguments_delta # => '{"city":'
68
+ ```
69
+
70
+ The `name` may only be present in the first delta. Accumulate `arguments_delta` to build the complete arguments.
71
+
72
+ ### ToolCallDone
73
+
74
+ Emitted when a tool call is complete:
75
+
76
+ ```ruby
77
+ event = Riffer::StreamEvents::ToolCallDone.new(
78
+ item_id: "item_123",
79
+ call_id: "call_456",
80
+ name: "weather_tool",
81
+ arguments: '{"city":"Tokyo"}'
82
+ )
83
+ event.role # => "assistant"
84
+ event.item_id # => "item_123"
85
+ event.call_id # => "call_456"
86
+ event.name # => "weather_tool"
87
+ event.arguments # => '{"city":"Tokyo"}'
88
+ ```
89
+
90
+ Contains the complete tool call information.
91
+
92
+ ### ReasoningDelta
93
+
94
+ Emitted when reasoning/thinking content is streamed (OpenAI with reasoning enabled):
95
+
96
+ ```ruby
97
+ event = Riffer::StreamEvents::ReasoningDelta.new("Let me think about ")
98
+ event.role # => "assistant"
99
+ event.content # => "Let me think about "
100
+ ```
101
+
102
+ ### ReasoningDone
103
+
104
+ Emitted when reasoning is complete:
105
+
106
+ ```ruby
107
+ event = Riffer::StreamEvents::ReasoningDone.new("Let me think about this step by step...")
108
+ event.role # => "assistant"
109
+ event.content # => "Let me think about this step by step..."
110
+ ```
111
+
112
+ ## Streaming with Tools
113
+
114
+ When an agent uses tools during streaming, the flow is:
115
+
116
+ 1. Text events stream in (`TextDelta`, `TextDone`)
117
+ 2. If tool calls are present: `ToolCallDelta` events, then `ToolCallDone`
118
+ 3. Agent executes tools internally
119
+ 4. Agent sends results back to LLM
120
+ 5. More text events stream in
121
+ 6. Repeat until no more tool calls
122
+
123
+ ```ruby
124
+ agent.stream("What's the weather in Tokyo?").each do |event|
125
+ case event
126
+ when Riffer::StreamEvents::TextDelta
127
+ print event.content
128
+ when Riffer::StreamEvents::ToolCallDone
129
+ puts "\n[Calling #{event.name}...]"
130
+ when Riffer::StreamEvents::TextDone
131
+ puts "\n"
132
+ end
133
+ end
134
+ ```
135
+
136
+ ## Complete Example
137
+
138
+ ```ruby
139
+ class WeatherAgent < Riffer::Agent
140
+ model 'openai/gpt-4o'
141
+ instructions 'You are a weather assistant.'
142
+ uses_tools [WeatherTool]
143
+ end
144
+
145
+ agent = WeatherAgent.new
146
+ text_buffer = ""
147
+
148
+ agent.stream("What's the weather in Tokyo and New York?").each do |event|
149
+ case event
150
+ when Riffer::StreamEvents::TextDelta
151
+ print event.content
152
+ text_buffer += event.content
153
+
154
+ when Riffer::StreamEvents::TextDone
155
+ # Final text available
156
+ puts "\n---"
157
+ puts "Complete response: #{event.content}"
158
+
159
+ when Riffer::StreamEvents::ToolCallDelta
160
+ # Could show "typing..." indicator
161
+
162
+ when Riffer::StreamEvents::ToolCallDone
163
+ puts "\n[Tool: #{event.name}(#{event.arguments})]"
164
+
165
+ when Riffer::StreamEvents::ReasoningDelta
166
+ # Show thinking process if desired
167
+ print "[thinking] #{event.content}"
168
+
169
+ when Riffer::StreamEvents::ReasoningDone
170
+ puts "\n[reasoning complete]"
171
+ end
172
+ end
173
+ ```
174
+
175
+ ## Base Class
176
+
177
+ All events inherit from `Riffer::StreamEvents::Base`:
178
+
179
+ ```ruby
180
+ class Riffer::StreamEvents::Base
181
+ attr_reader :role
182
+
183
+ def initialize(role: "assistant")
184
+ @role = role
185
+ end
186
+
187
+ def to_h
188
+ raise NotImplementedError
189
+ end
190
+ end
191
+ ```