llm.rb 16.0.0 → 17.0.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +510 -2
  3. data/README.md +857 -501
  4. data/data/alibaba.json +2 -2
  5. data/data/anthropic.json +66 -3
  6. data/data/bedrock.json +781 -12
  7. data/data/deepinfra.json +157 -1
  8. data/data/google.json +48 -0
  9. data/data/mistral.json +326 -16
  10. data/data/moonshot.json +2 -1
  11. data/data/openai.json +13 -0
  12. data/data/openrouter.json +1078 -1235
  13. data/data/xai.json +4 -2
  14. data/docs/deepdive/advanced/cancellation.md +35 -7
  15. data/docs/deepdive/advanced/compaction.md +5 -5
  16. data/docs/deepdive/advanced/context.md +45 -24
  17. data/docs/deepdive/advanced/{guard.md → intercept.md} +87 -83
  18. data/docs/deepdive/features/builtin_tools.md +1 -1
  19. data/docs/deepdive/features/concurrency.md +15 -10
  20. data/docs/deepdive/features/database.md +19 -19
  21. data/docs/deepdive/features/embeddings.md +4 -4
  22. data/docs/deepdive/features/files.md +7 -5
  23. data/docs/deepdive/fundamentals/agents.md +13 -8
  24. data/docs/deepdive/fundamentals/providers.md +1 -1
  25. data/docs/deepdive/fundamentals/schema.md +2 -2
  26. data/docs/deepdive/fundamentals/stream.md +8 -8
  27. data/docs/deepdive/fundamentals/tools.md +14 -14
  28. data/docs/deepdive/media/ocr.md +1 -1
  29. data/docs/deepdive/protocols/a2a.md +3 -3
  30. data/docs/deepdive/protocols/mcp.md +116 -7
  31. data/docs/deepdive/reference/cost.md +1 -1
  32. data/docs/deepdive/reference/response.md +1 -1
  33. data/docs/deepdive/reference/tracer.md +90 -22
  34. data/docs/deepdive.md +1 -1
  35. data/lib/llm/agent/interrupt.rb +35 -46
  36. data/lib/llm/agent/registry.rb +106 -0
  37. data/lib/llm/agent.rb +244 -269
  38. data/lib/llm/context.rb +63 -55
  39. data/lib/llm/function/array.rb +2 -1
  40. data/lib/llm/function/async/group.rb +2 -2
  41. data/lib/llm/function/async/task.rb +25 -48
  42. data/lib/llm/function/fiber/group.rb +2 -2
  43. data/lib/llm/function/fiber/task.rb +16 -30
  44. data/lib/llm/function/fork/group.rb +2 -2
  45. data/lib/llm/function/fork/job.rb +8 -4
  46. data/lib/llm/function/fork/task.rb +25 -30
  47. data/lib/llm/function/group.rb +33 -5
  48. data/lib/llm/function/ractor/group.rb +2 -2
  49. data/lib/llm/function/ractor/job.rb +8 -6
  50. data/lib/llm/function/ractor/task.rb +32 -9
  51. data/lib/llm/function/sequential/group.rb +2 -1
  52. data/lib/llm/function/sequential/task.rb +8 -1
  53. data/lib/llm/function/task.rb +52 -4
  54. data/lib/llm/function/thread/group.rb +2 -2
  55. data/lib/llm/function/thread/task.rb +36 -10
  56. data/lib/llm/function/window.rb +16 -0
  57. data/lib/llm/function.rb +14 -43
  58. data/lib/llm/intercept/multi.rb +40 -0
  59. data/lib/llm/intercept/null.rb +18 -0
  60. data/lib/llm/intercept.rb +96 -0
  61. data/lib/llm/message.rb +1 -1
  62. data/lib/llm/provider.rb +16 -1
  63. data/lib/llm/providers/anthropic/stream_parser.rb +1 -1
  64. data/lib/llm/providers/bedrock/stream_parser.rb +1 -1
  65. data/lib/llm/providers/deepseek/files.rb +49 -0
  66. data/lib/llm/providers/deepseek/request_adapter/completion.rb +15 -13
  67. data/lib/llm/providers/deepseek.rb +16 -2
  68. data/lib/llm/providers/google/stream_parser.rb +1 -1
  69. data/lib/llm/providers/openai/responses/stream_parser.rb +1 -1
  70. data/lib/llm/providers/openai/stream_parser.rb +1 -1
  71. data/lib/llm/stream.rb +8 -7
  72. data/lib/llm/tool.rb +1 -1
  73. data/lib/llm/tools/utils.rb +1 -1
  74. data/lib/llm/tracer/logger.rb +42 -1
  75. data/lib/llm/tracer/multi.rb +144 -0
  76. data/lib/llm/tracer/null.rb +20 -0
  77. data/lib/llm/tracer/pretty_logger.rb +33 -2
  78. data/lib/llm/tracer/rescue.rb +110 -0
  79. data/lib/llm/tracer/telemetry.rb +69 -4
  80. data/lib/llm/tracer.rb +125 -48
  81. data/lib/llm/utils.rb +12 -2
  82. data/lib/llm/version.rb +1 -1
  83. data/lib/llm.rb +36 -1
  84. data/llm.gemspec +9 -4
  85. metadata +16 -35
  86. data/lib/llm/function/tracing.rb +0 -21
  87. data/lib/llm/guard/null.rb +0 -19
  88. data/lib/llm/guard.rb +0 -55
data/README.md CHANGED
@@ -18,16 +18,9 @@ llm.rb is a runtime for building agentic AI applications
18
18
  on CRuby. It has zero runtime dependencies by default, supports
19
19
  concurrent and parallel tool execution and has a single coherent API
20
20
  that spans 14+ providers.
21
- The README covers the common cases. For everything else there is the
22
- [deepdive](docs/deepdive.md), a reference with a chapter for each
23
- topic, and for what changes between releases there is the
24
- [changelog](CHANGELOG.md).
25
-
26
- [The r.uby.dev website](https://r.uby.dev) hosts
27
- an agentic platform that provides users with
28
- personalized agents who can access GitHub, and
29
- other services. It is built with llm.rb. Check it
30
- out if curious. **Still in early development.**
21
+ The README covers a lot of ground and the
22
+ [changelog](CHANGELOG.md)
23
+ tracks what has changed between releases.
31
24
 
32
25
  ## Install
33
26
 
@@ -46,101 +39,191 @@ The
46
39
  class is the default high-level interface,
47
40
  and it is recommended for most use-cases. It manages the tool loop
48
41
  and provides configurable features on top of it. For example you can
49
- manage the tool loop with a retry budget alongside a tool call budget,
50
- among other features.
42
+ manage the tool loop with a retry budget and a tool call budget -
43
+ alongside other features.
51
44
 
52
45
  The runtime is designed to keep the tool loop alive and it will
53
- avoid exceptions. When an error is encountered in a tool or during
54
- the lifecycle of an agent it is almost always reported back to the
55
- model as an in-band error that allows the model to correct course.
56
-
57
- A lot of care also goes into keeping the tool loop from entering
58
- an invalid state that would lead to API-level errors. For example,
59
- when a tool call is interrupted it could leave an unanswered tool
60
- call that a model will reject on the next turn. The runtime closes
61
- every tool call that has no return before the next request is sent,
62
- and each one is answered with an in-band return of its own. The
63
- conversation a provider sees is therefore always valid, and a
64
- cancelled call is something the model is told about rather than
65
- something that quietly disappears.
46
+ rescue exceptions. When an exception is encountered in a tool it
47
+ is reported back to the model as an in-band error that allows the
48
+ model to change course or retry with different parameters.
49
+
50
+ A tool call requires a tool return (or response), and the lack of
51
+ one can corrupt the conversation and lead to API-level errors from
52
+ a provider. But sometimes it is unavoidable (for example, via an
53
+ interrupt or power loss) so the runtime automatically closes
54
+ tool calls that fall into that category by telling the model
55
+ the tool call(s) were cancelled.
56
+
57
+ ##### helloworld.rb
58
+
59
+ A classic "hello world" example. <br> The environment variable
60
+ `$DEEPSEEK_API_KEY` is assumed to have been set and reference
61
+ a valid DeepSeek API key, otherwise you can provide a key explicitly
62
+ (eg: `LLM.deepseek(key: ENV["MYKEY"])`) and you could swap
63
+ deepseek for any of the other 13 providers as well
64
+ (eg: `llm = LLM.openai`):
66
65
 
67
66
  ```ruby
68
67
  require "llm"
69
68
 
70
- llm = LLM.deepseek(key: ENV["KEY"])
71
- agent = LLM::Agent.new(llm, stream: $stdout)
69
+ class GetTime < LLM::Tool
70
+ name "get-time"
71
+ description "returns the current time"
72
+
73
+ def call
74
+ {now: Time.now}
75
+ end
76
+ end
77
+
78
+ llm = LLM.deepseek
79
+ tracer = LLM::Tracer.pretty(llm)
80
+ tools = [GetTime]
81
+ stream = StringIO.new
82
+
83
+ agent = LLM::Agent.new(llm, tools:, tracer:, stream:)
72
84
  agent.talk "hello world"
85
+ agent.talk "what time is it?"
86
+ puts stream.string
73
87
  ```
88
+
89
+ #### Essentials
90
+
74
91
  <details>
75
92
  <summary>Stream</summary>
76
93
  <br>
77
94
 
78
- Streams can be simple IO objects or subclasses of
79
- [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
80
- with structured callbacks for content,
81
- reasoning, tool calls, tool returns, steps in a turn, and compaction.
82
- Streams can also observe message transformers, which rewrite
83
- outgoing messages before they reach the provider.
95
+ A stream can be a simple IO object (eg `$stdout`)
96
+ or it can be a subclass of [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html).
97
+ An IO object can receive content but it cannot
98
+ receive the other callbacks that are available
99
+ to subclasses of [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html).
100
+ It is generally a good idea to return as
101
+ quickly as possible from all of these methods,
102
+ but especially for the `on_content` and
103
+ `on_reasoning_content` methods: they run
104
+ inline with the request.
105
+
106
+ A subclass of [`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
107
+ can implement callbacks that the runtime will call
108
+ throughout an agent's lifetime. All callbacks are
109
+ optional. The callbacks provide for content, reasoning,
110
+ tool calls, tool returns, steps in a turn, retries,
111
+ compaction and more:
84
112
 
85
113
  ```ruby
86
- class MyStream < LLM::Stream
87
- # Visible assistant output.
114
+ class Stream < LLM::Stream
115
+ ##
116
+ # @param [String] content
117
+ # A chunk of text
88
118
  def on_content(content)
89
119
  print content
90
120
  end
91
121
 
92
- # Reasoning output streamed separately from visible content.
122
+ ##
123
+ # @param [String] content
124
+ # A chunk of text
93
125
  def on_reasoning_content(content)
94
126
  warn content
95
127
  end
96
128
 
97
- # A streamed tool call has been fully parsed.
129
+ ##
130
+ # @param [LLM::Function] tool
131
+ # The tool being called
98
132
  def on_tool_call(tool)
133
+ nil
99
134
  end
100
135
 
101
- # Queued streamed tool work has returned.
136
+ ##
137
+ # @param [LLM::Function] tool
138
+ # The tool that returned
139
+ # @param [LLM::Function::Return] result
140
+ # The return from the tool call
102
141
  def on_tool_return(tool, result)
142
+ nil
103
143
  end
104
144
 
105
- # A request has completed: the response is in the conversation, and
106
- # any tools it asked for run after this.
107
- def on_step(ctx, res)
108
- end
109
-
110
- # Before a transformer rewrites an outgoing message.
145
+ ##
146
+ # @note
147
+ # This method is called _before_ a transformer runs
148
+ # @param [LLM::Transformer] transformer
149
+ # A transformer
111
150
  def on_transform(transformer)
151
+ nil
112
152
  end
113
153
 
114
- # Aftter a transformer rewrites an outgoing message.
154
+ ##
155
+ # @note
156
+ # This method is called _after_ a transformer runs
157
+ # @param [LLM::Transformer] transformer
158
+ # A transformer
115
159
  def on_transform_finish(transformer)
160
+ nil
116
161
  end
117
162
 
118
- # Before a compactor trims the conversation.
163
+ ##
164
+ # @note
165
+ # This method is called _before_ a compactor runs
166
+ # @param [LLM::Compactor] compactor
167
+ # A compactor
119
168
  def on_compaction(compactor)
169
+ nil
120
170
  end
121
171
 
122
- # After a compactor trims the conversation.
172
+ ##
173
+ # @note
174
+ # This method is called _after_ a compactor runs
175
+ # @param [LLM::Compactor] compactor
176
+ # A compactor
123
177
  def on_compaction_finish(compactor)
178
+ nil
124
179
  end
125
180
 
126
- # Before a skill's subagent runs.
127
- def on_skill_call(skill)
181
+ ##
182
+ # @note
183
+ # This method is called once per request
184
+ # in a turn (which can contain multiple
185
+ # requests)
186
+ # @param [LLM::Context] ctx
187
+ # The context
188
+ # @param [LLM::Response] res
189
+ # The response
190
+ def on_step(ctx, res)
191
+ nil
128
192
  end
129
193
 
130
- # After a skill's subagent runs.
131
- # The subagent that ran it, the skill, and its response are passed
132
- # through, so you can introspect the agent, tally skill usage, or
133
- # track costs.
134
- def on_skill_return(agent, skill, result)
194
+ ##
195
+ # @note
196
+ # This method is called when a request is
197
+ # rate limited and retried.
198
+ # @param [LLM::RateLimitError] error
199
+ # @param [Integer] attempt
200
+ def on_retry(error, attempt)
201
+ nil
135
202
  end
136
203
 
137
- # A request was rate limited or timed out and will be retried.
138
- def on_retry(error, attempt)
204
+ ##
205
+ # @note
206
+ # This method is called _before_ a skill runs
207
+ # @param [LLM::Skill] skill
208
+ # A skill
209
+ def on_skill_call(skill)
210
+ nil
211
+ end
212
+
213
+ ##
214
+ # @note
215
+ # This method is called _after_ a skill runs
216
+ # @param [LLM::Agent] agent
217
+ # The agent who ran the skill
218
+ # @param [LLM::Skill] skill
219
+ # @param [LLM::Response] res
220
+ def on_skill_return(agent, skill, res)
221
+ nil
139
222
  end
140
223
  end
141
224
 
142
- llm = LLM.deepseek(key: ENV["KEY"])
143
- agent = LLM::Agent.new(llm, stream: MyStream.new)
225
+ llm = LLM.deepseek
226
+ agent = LLM::Agent.new(llm, stream: Stream.new)
144
227
  agent.talk "Explain Ruby fibers."
145
228
  ```
146
229
  </details>
@@ -148,17 +231,23 @@ agent.talk "Explain Ruby fibers."
148
231
  <details><summary>Tools</summary>
149
232
  <br>
150
233
 
151
- Subclasses of
152
- [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
153
- are plain Ruby classes with
154
- an optional set of typed parameters. <br> The model can choose to
155
- call them on your behalf, and they're one of the most powerful features
156
- for extending the feature set or abilities of a model.
157
-
158
- The runtime also ships with a catalog of built-in tools for
159
- filesystem, search, and shell operations, and providers expose
160
- platform-native tools such as web search and code execution that run
161
- on the provider's side.
234
+ An agent requires one or more tools to be
235
+ able to interact with the "outside" world.
236
+ At a high-level a tool is how a model can
237
+ access your filesystem, search the internet,
238
+ post a comment on your behalf and anything
239
+ else that your own code could do.
240
+
241
+ The runtime represents a tool as a subclass
242
+ of [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
243
+ that provides a name, a description, an optional
244
+ set of parameters and a method that the runtime
245
+ will call on the model's behalf. A tool is implemented
246
+ on top of
247
+ an [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
248
+ object, and you might come across it in stream and
249
+ tracer callbacks. The model decides when and how a
250
+ tool is called:
162
251
 
163
252
  ```ruby
164
253
  class ReadFile < LLM::Tool
@@ -172,255 +261,441 @@ class ReadFile < LLM::Tool
172
261
  end
173
262
  end
174
263
 
175
- llm = LLM.deepseek(key: ENV["KEY"])
264
+ llm = LLM.deepseek
176
265
  agent = LLM::Agent.new(llm, tools: [ReadFile], stream: $stdout)
177
266
  agent.talk "summarize README.md"
178
267
  ```
179
268
  </details>
180
- <details>
181
- <summary>Skills</summary>
269
+ <details><summary>Schema</summary>
182
270
  <br>
183
271
 
184
- A skill turns a markdown file into a callable tool. When the model
185
- calls it, the runtime spawns a subagent with the skill's instructions
186
- as its system prompt and the skill's own tool set. The subagent runs
187
- one turn and returns the result, then is discarded. Each call
188
- is fresh and stateless.
189
-
190
- A [LLM::Stream](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
191
- can be notified as a skill starts and when it returns. The `on_skill_return`
192
- callback hands back the subagent that ran the skill, so you can inspect
193
- its conversation, measure its usage, track costs or add a verification
194
- step (eg `subagent.talk("verify your work")`).
272
+ The
273
+ [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
274
+ class describes an object that has properties
275
+ of different types, and it can also reference
276
+ other objects, or schemas. The object describes
277
+ the shape of a JSON object that the model will
278
+ return in place of the usual free-form text that
279
+ a model replies with.
280
+
281
+ This feature is implemented with
282
+ [JSONSchema](https://json-schema.org/)
283
+ under the hood and a large portion
284
+ of the spec is supported: objects, enums,
285
+ arrays, nested objects, strings, integers,
286
+ and so on.
287
+ [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
288
+ also provides the parameter definitions
289
+ available to
290
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
291
+ subclasses and the same range of types are
292
+ supported.
195
293
 
196
- ##### summary.md
294
+ The
295
+ [`LLM::DeepSeek`](https://r.uby.dev/api-docs/llm.rb/LLM/DeepSeek.html)
296
+ provider does not support structured outputs
297
+ officially but the runtime implements structured
298
+ outputs on top of the `json_object` API and it
299
+ works the same as all other providers who do
300
+ support structured outputs:
197
301
 
198
- ```markdown
199
- ---
200
- name: summary
201
- description: Reads recent git history and writes a summary
202
- tools: all
203
- ---
302
+ ```ruby
303
+ class Forecast < LLM::Schema
304
+ property :city, String, "The city name"
305
+ property :temperature, Number, "Current temperature"
306
+ property :conditions, String, "Weather conditions"
307
+ required %i[city temperature conditions]
308
+ end
204
309
 
205
- Collect the recent git log, analyze each commit,
206
- and write a summary to summary.txt.
310
+ llm = LLM.deepseek
311
+ agent = LLM::Agent.new(llm)
312
+ res = agent.talk "Weather in Paris?", schema: Forecast
313
+ res.content! # => {city: "Paris", temperature: 15.0, conditions: "Cloudy"}
207
314
  ```
315
+ </details>
316
+ <details>
317
+ <summary>Serialization</summary>
318
+ <br>
208
319
 
209
- ##### agent.rb
320
+ Both [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
321
+ and
322
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
323
+ can be serialized to JSON and written to disk.
324
+ This feature is what supports the ActiveRecord
325
+ and Sequel integrations too but rather than
326
+ store the agent directly on disk it is stored
327
+ in a database column instead.
328
+
329
+ An agent can be configured to read from and
330
+ write to a file automatically with the `path`
331
+ option. When the file already exists, the agent
332
+ is restored from the file and continues where
333
+ he left off. After each turn the agent flushes
334
+ its state to the file. The text file can be shared
335
+ like any other text file and it can be used to
336
+ restore the agent in another process or machine:
210
337
 
211
338
  ```ruby
212
339
  require "llm"
213
340
 
214
- llm = LLM.deepseek(key: ENV["KEY"])
215
- agent = LLM::Agent.new(llm, skills: ["summary.md"])
216
- agent.talk "Summarize the last week of work"
341
+ path = File.join(Dir.home, ".agents", "myagent.json")
342
+ llm = LLM.deepseek
343
+ agent = LLM::Agent.new(llm, path:)
344
+ agent.talk "remember my name is robert"
345
+
346
+ ##
347
+ # Resume the conversation where the agent left off
348
+ agent = LLM::Agent.new(llm, path:)
349
+ agent.talk "what's my name?"
217
350
  ```
218
351
  </details>
219
-
220
352
  <details>
221
- <summary>Concurrency</summary>
353
+ <summary>Tracer</summary>
222
354
  <br>
223
355
 
224
- The runtime supports six different concurrency strategies that have
225
- different attributes. The choice between all of them often depends
226
- on the requirements of your application.
356
+ An agent can have its own tracer that observes runtime
357
+ events throughout its lifetime. A tracer is local to an
358
+ agent and the tracer runs on the same thread as the agent.
359
+ There are three builtin tracers, and a fourth tracer that
360
+ lets you combine multiple tracers into a single tracer -
361
+ so you can log to a file, and export to OpenTelemetry
362
+ at the same time.
363
+
364
+ All tracers protect themselves and the agent being traced
365
+ from a crash through a rescue that dumps a tracer error
366
+ to standard error (`$stderr`). Beyond the default tracers,
367
+ it is not unusual to write your own. A tracer can let you
368
+ export runtime state to your own database, an external
369
+ service, or any other medium you might have in your stack.
370
+ A new tracer can be created through a subclass of
371
+ [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
372
+ that implements the expected callback methods. This feature
373
+ also works for ActiveRecord and Sequel models.
374
+
375
+ The builtin tracers:
376
+
377
+ * [`LLM::Tracer.pretty`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#pretty_logger-class_method): human-readable single-line logs to stderr, ideal during development.
378
+ * [`LLM::Tracer.telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#telemetry-class_method):
379
+ exports spans via OTLP for OpenTelemetry in production.
380
+ * [`LLM::Tracer.logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#logger-class_method):
381
+ structured JSON to stdout or a file.
382
+ * [`LLM::Tracer.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#multi-class_method):
383
+ fans out every callback to multiple tracers.
384
+
385
+ A simple example:
227
386
 
228
- IO-bound tools are a good fit for the `:async`, `:thread`,
229
- and `:fiber` strategies while true parallelism can be achieved
230
- with the `:fork` and `:ractor` strategies. The
231
- `:sequential` strategy runs tools one at a time and is the default.
232
- The `:fork` strategy also provides a separate process that offers
233
- isolation from its parent.
387
+ ```ruby
388
+ llm = LLM.deepseek
389
+ tracer = LLM::Tracer.pretty(llm)
390
+ agent = LLM::Agent.new(llm, tracer:)
391
+ agent.talk "Hello"
392
+ ```
234
393
 
235
- A couple of concurrency strategies require optional, opt-in dependencies.
236
- The `async` strategy requires the [async](https://github.com/socketry/async)
237
- gem and the `fork` strategy requires the [xchan.rb](https://github.com/r-uby-dev/xchan.rb)
238
- gem (`~> 0.24`). The `fiber` strategy requires a scheduler (`Fiber.scheduler`) but by
239
- default Ruby does not provide one.
394
+ A multi-tracer example:
395
+
396
+ ```ruby
397
+ llm = LLM.deepseek
398
+ tracer = LLM::Tracer.multi(
399
+ llm,
400
+ LLM::Tracer::Logger,
401
+ LLM::Tracer::Telemetry,
402
+ LLM::Tracer::Foobar.new(llm, {option1: "foo", option2: "bar"})
403
+ )
404
+ agent = LLM::Agent.new(llm, tracer:)
405
+ agent.talk "Hello"
406
+ ```
240
407
 
241
- The `:ractor` strategy is the least interchangeable of the six. It runs
242
- class-based tools only, and a tool's arguments have to be
243
- ractor-shareable.
408
+ </details>
409
+
410
+ <details><summary>MCP</summary>
411
+ <br>
412
+
413
+ The runtime can communicate with MCP servers via HTTP(S)
414
+ and stdio. The stdio transport spawns a local process that
415
+ uses its standard input and output streams for communication.
416
+ The runtime provides a full-featured MCP client and the tools
417
+ that an MCP server has announced are available as
418
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
419
+ subclasses that can be used like any other subclass
420
+ of
421
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html).
422
+
423
+
424
+ The first example uses the HTTP transport to connect to
425
+ GitHub's MCP server. The example uses a Personal Access
426
+ Token (get yours [here](https://github.com/settings/apps))
427
+ although it could also use a token that was obtained
428
+ through OAuth and belongs to a user other than you:
244
429
 
245
430
  ```ruby
246
431
  require "llm"
247
- require "llm/tools"
248
432
 
249
- llm = LLM.deepseek(key: ENV["KEY"])
250
- tools = LLM::Tool.subclasses
251
- agent = LLM::Agent.new(llm, tools:, concurrency: :fork)
252
- agent.talk "Run the tools in parallel"
433
+ llm = LLM.deepseek
434
+ mcp = LLM::MCP.http(
435
+ url: "https://api.githubcopilot.com/mcp/",
436
+ headers: {"Authorization" => "Bearer #{ENV['GITHUB_PAT']}"},
437
+ transport: :net_http_persistent
438
+ )
439
+
440
+ agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
441
+ agent.talk "Find me an interesting Ruby project on GitHub"
253
442
  ```
254
443
 
255
- </details>
256
- <details>
257
- <summary>Cancellation</summary>
258
- <br>
444
+ The second example focuses on the second transport.
445
+ It is recommended (but not required) to launch a
446
+ stdio MCP server with a session so that the process
447
+ is kept alive for as long as you use it, otherwise
448
+ the process can be launched multiple times and it
449
+ can add latency to your requests.
259
450
 
260
- It is possible to abort a request mid-stream and interrupt
261
- running tool calls with
262
- [`LLM::Agent#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#interrupt!)
263
- (or `cancel!`) as long as it is done by another thread or fiber in
264
- the same process.
265
-
266
- The runtime raises
267
- [`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
268
- on the fiber making a request and on every active tool
269
- regardless of the active concurrency strategy. Before that
270
- happens the request socket is forcibly closed so the request
271
- stops burning tokens. It works the same across all strategies
272
- although there can be subtle differences that usually go unnoticed.
273
-
274
- A tool can implement the `#on_interrupt` callback
275
- to be notified when a tool call has been interrupted
276
- and it can also rescue the `LLM::Interrupt` exception
277
- to free resources or perform other actions before the
278
- tool dies.
451
+ The example uses a local stdio MCP server from the
452
+ [sem](https://github.com/Ataraxy-Labs/sem)
453
+ project. <br> It works on top of git and parses your code
454
+ with tree-sitter:
279
455
 
280
456
  ```ruby
281
- llm = LLM.deepseek(key: ENV["KEY"])
282
- agent = LLM::Agent.new(llm)
283
- Thread.new { sleep(1); agent.cancel! }
457
+ require "llm"
284
458
 
285
- begin
286
- agent.talk "write a very long poem", stream: $stdout
287
- rescue LLM::Interrupt
288
- puts "cancelled"
459
+ mcp = LLM::MCP.stdio(
460
+ argv: ["sem", "mcp"],
461
+ env: {'SEM_REPO' => Dir.getwd},
462
+ cwd: Dir.getwd,
463
+ )
464
+
465
+ ##
466
+ # 'mcp.session' is optional, but recommended.
467
+ # It can be dropped but then the process may
468
+ # be launched once per-request.
469
+ mcp.session do
470
+ llm = LLM.deepseek
471
+ tracer = LLM::Tracer.pretty(llm)
472
+ tools = mcp.tools.select { _1.name == "sem_log" }
473
+ stream = $stdout
474
+ agent = LLM::Agent.new(llm, tracer:, stream:, tools:)
475
+ agent.talk "summarize recent git activity"
289
476
  end
290
477
  ```
291
478
  </details>
479
+
292
480
  <details>
293
- <summary>Console (<code>binding.irb</code> for agents)</summary>
481
+ <summary>Console</summary>
294
482
  <br>
295
483
 
296
484
  The [LLM::Agent#console](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#console-instance_method)
297
485
  method drops you into an interactive console that is built on
298
- top of curses. It can help you debug agents, test your tools,
299
- connect to MCP servers, and other A2A agents. The console stands
300
- out because it connects to the surrounding runtime and it can
301
- be extended by your code. Think of it as `binding.irb` but
302
- for agents.
486
+ top of (n)curses. The `llm.rb` executable packaged with the gem
487
+ is another way to access the console and ActiveRecord models who
488
+ have called `acts_as_agent` can access the console as well
489
+ (via `agent.console`).
490
+
491
+ A console for an ActiveRecord model does not write back to the
492
+ database. The `llm.rb` executable automatically associates a
493
+ session with the current working directory and it can be resumed
494
+ by calling `llm.rb` in the same directory at a later point.
495
+
496
+ The console is not intended to compete with Claude, Codex and
497
+ friends. It is much more limited, serves an entirely different
498
+ purpose and is more like a debugger for your agents. The
499
+ dependencies required by the console are not installed
500
+ by default, and the easiest way to grab them is via
501
+ `gem install llm-shell`.
303
502
 
304
503
  ##### Demo
305
504
 
306
505
  ![llm.rb console demo](demo.gif)
307
506
 
507
+ </details>
508
+ <details><summary>Budgets</summary>
509
+ <br>
308
510
 
309
- ##### Installation
310
-
311
- The console is distributed with llm.rb but it requires a number
312
- of optional dependencies to be installed separately. The following
313
- gems provide the full experience:
314
-
315
- gem install unicode-display_width curses kramdown xchan.rb test-cmd.rb
511
+ An agent has a retry budget, and a tool call
512
+ budget. The retry budget is enabled by default,
513
+ and it determines how many times we will retry
514
+ when a request raises an error. The errors
515
+ that are retried are returned by
516
+ [`LLM::Provider#retry_errors`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#retry_errors-instance_method)
517
+ and can vary by provider but typically cover
518
+ rate limits, and time outs. Between retries
519
+ we will sleep, and each retry sleeps a little
520
+ longer (`sleep 2.0 * attempts`) than the last.
521
+
522
+ The default retry budget is usually 5 but
523
+ it can also vary by provider and it is determined by the
524
+ [`LLM::Provider#retry_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#retry_budget-instance_method)
525
+ method. When the budget is spent but the an
526
+ exception continues to raise we will give up
527
+ and let the exception bubble up to the caller.
528
+ The budget can be disabled by setting
529
+ `retry_budget` to zero:
316
530
 
317
- For convenience it is also possible to just use the following, it
318
- is a metagem that depends on llm.rb and all the dependencies it requires
319
- to run the console:
531
+ ```ruby
532
+ require "llm"
320
533
 
321
- gem install llm-shell
534
+ llm = LLM.deepseek
535
+ agent = LLM::Agent.new(llm, retry_budget: 0)
536
+ ```
322
537
 
323
- ##### Persistence
538
+ The second budget describes how many tool calls
539
+ are allowed in a single turn, and it is disabled
540
+ by default. When the limit is reached or breached
541
+ the agent will reply with an in-band error that
542
+ tells the model it has spent its tool call budget,
543
+ and that it should stop requesting tool calls for
544
+ this turn. This exchange repeats until the model
545
+ stops requesting tool calls.
324
546
 
325
- the `path:` option can be set on an agent for automatic persistence
326
- across console sessions. The `tools:` option attaches extra tools
327
- for the duration of the session. Recall previous turns with Ctrl+P and
328
- Ctrl+N.
547
+ The budget is restored after a turn completes and the
548
+ model will normally provide a response from the tool
549
+ calls made before the budget was spent:
329
550
 
330
551
  ```ruby
331
552
  require "llm"
332
553
  require "llm/tools"
333
554
 
334
- llm = LLM.deepseek(key: ENV["KEY"])
335
- agent = LLM::Agent.new(llm, name: "my-agent", path: "agent.json")
336
- agent.console(tools: LLM::Tool.subclasses)
555
+ llm = LLM.deepseek
556
+ tools = LLM::Tool.subclasses
557
+ agent = LLM::Agent.new(llm, tools:, tool_budget: 25)
558
+ agent.talk "fix every FIXME in this repository"
337
559
  ```
560
+ </details>
561
+ <details>
562
+ <summary>Usage|Cost</summary>
563
+ <br>
338
564
 
339
- ##### CLI
565
+ An agent knows how many tokens it has spent,
566
+ and how much those tokens cost. The runtime
567
+ embeds the [models.dev](https://models.dev)
568
+ registry (only the parts it uses, and on-demand)
569
+ that gives it access to model information that
570
+ includes costs and limits.
571
+
572
+ The `token_usage` method sums the whole conversation as a
573
+ [LLM::Usage](https://r.uby.dev/api-docs/llm.rb/LLM/Usage.html)
574
+ object and [LLM::Cost](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html)
575
+ combines it with the model registry to determine how much money
576
+ has been spent. These numbers should not be taken as gospel though.
577
+ They're a best-guess estimate, and your provider's dashboard
578
+ might disagree:
340
579
 
341
- The `llm.rb` executable is available on your PATH after installation.
342
- It starts a console session from any directory. The CLI auto-detects your
343
- provider from standard environment variables (`DEEPSEEK_API_KEY`,
344
- `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.). Persistent sessions are
345
- stored under `~/.llm.rb/` and restored automatically on your next visit.
580
+ ```ruby
581
+ require "llm"
346
582
 
347
- ```bash
348
- llm.rb # auto-detect from $PROVIDER_API_KEY
349
- llm.rb -p openai # use OpenAI explicitly
350
- llm.rb -m gpt-5.6 # use a model other than the provider default
351
- llm.rb -c thread # run tool calls on a separate thread
352
- llm.rb -n curb # use libcurl as the HTTP transport
353
- llm.rb -x 900 # read timeout of 15 minutes
354
- llm.rb -t # temporary session, no persistence
355
- llm.rb -v # print the version
356
- llm.rb -h # print usage
583
+ llm = LLM.deepseek
584
+ agent = LLM::Agent.new(llm)
585
+ agent.talk "Hello"
586
+
587
+ agent.token_usage # => LLM::Usage for the whole conversation
588
+ agent.cost # => LLM::Cost, priced from the registry
357
589
  ```
590
+
591
+ The `context_*` methods can tell you about the model's context
592
+ window: its total capacity, and how much of it have we already
593
+ spent:
594
+
595
+ ```ruby
596
+ agent.context_used # => tokens in the latest turn
597
+ agent.context_window # => the model's limit, or nil when unknown
598
+ agent.context_usage # => Rational, eg Rational(100, 10_000)
599
+ ```
600
+
358
601
  </details>
359
602
  <details>
360
- <summary>Persistence</summary>
603
+ <summary><code>class Agent < LLM::Agent</code></summary>
361
604
  <br>
362
605
 
363
- Set `path:` on an agent for automatic filesystem persistence:
364
- the agent restores conversation history from the file on startup
365
- and saves it back after every turn, with no manual serialization
366
- code. For database-backed persistence, ActiveRecord and Sequel
367
- integrations are also available. All persistence options use the same
368
- underlying serialization.
606
+ The examples so far have focused on
607
+ [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
608
+ as an object but you can create a subclass that inherits from
609
+ [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
610
+ too. Both support the same features and accept the same options
611
+ but a subclass can be configured with the `set` method, implement
612
+ its own methods, inherit from other modules and create multiple
613
+ instances of itself.
614
+
615
+ The
616
+ [`LLM::Agent.set`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#set-class_method)
617
+ method can be used to configure your agent, and
618
+ it accepts all the same options that an instance
619
+ of [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
620
+ accepts. An option can be resolved lazily via
621
+ a Proc or a Symbol that represents a method
622
+ on your class, and all options are optional -
623
+ you can set many or none, and your subclass
624
+ will still work. For example:
369
625
 
370
626
  ```ruby
371
627
  require "llm"
628
+ require "llm/tools"
372
629
 
373
- llm = LLM.deepseek(key: ENV["KEY"])
374
- agent = LLM::Agent.new(llm, path: "session.json")
375
- agent.talk "remember my name is robert"
630
+ class Agent < LLM::Agent
631
+ set name: "sysadmin",
632
+ description: "performs system administration tasks",
633
+ instructions: proc { File.read("prompt.md") },
634
+ concurrency: :fork,
635
+ tools: :mytools
376
636
 
377
- # Next time, the conversation is restored automatically:
378
- agent = LLM::Agent.new(llm, path: "session.json")
379
- agent.talk "what's my name?"
637
+ private
638
+
639
+ def mytools
640
+ [LLM::Tool::Exec]
641
+ end
642
+ end
643
+
644
+ llm = LLM.deepseek
645
+ agent = Agent.new(llm)
646
+ agent.talk "What's our uptime?", stream: $stdout
380
647
  ```
381
648
  </details>
382
- <details><summary>ActiveRecord | Sequel</summary>
649
+
650
+ #### Database
651
+
652
+ <details><summary>ActiveRecord </summary>
383
653
  <br>
384
654
 
385
- Because both
655
+ Both
386
656
  [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) and
387
657
  [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
388
- can be serialized to JSON and stored in a simple string, both ActiveRecord
389
- and Sequel support can be implemented within a single column on a single row.
390
-
391
- The runtime includes first-class support for both ActiveRecord / Sequel, and
392
- for both Rack-based / Rails-based applications. On databases
393
- where it is supported, such as PostgreSQL, the column can be optimized by using
394
- the `jsonb` type.
658
+ can be serialized to JSON and stored in a database column. The `jsonb`
659
+ column type from PostgreSQL is recommended but it can also be stored as
660
+ a string on other databases. ActiveRecord support is optimized
661
+ for the `jsonb` column type and PostgreSQL.
662
+
663
+ The column captures everything an agent has done up to that point,
664
+ it includes tool calls and returns, exchanged messages, and other
665
+ metadata that carries runtime state. Each agent is an ActiveRecord
666
+ model that calls `acts_as_agent` and each row represents an instance
667
+ of that agent. It can be used with new and existing models alike.
668
+
669
+ The column should have the name `data` but this can be
670
+ changed when the `acts_as_agent` method is called (eg
671
+ `acts_as_agent(data_column: :my_column)`). The column
672
+ is updated after every request that an agent makes rather
673
+ than every turn, so an unexpected interrupt can be resumed
674
+ from from the last request and no progress (or spent tokens)
675
+ are lost:
395
676
 
396
677
  ```ruby
397
678
  require "active_record"
398
679
  require "llm"
399
680
  require "llm/active_record"
400
681
 
401
- ##
402
- # The Robert agent.
403
- class Robert < ActiveRecord::Base
682
+ class Octocat < ActiveRecord::Base
404
683
  acts_as_agent(format: :jsonb) do |agent|
405
- agent.set name: "robert",
406
- description: "robert is an agent that has access to the official " \
407
- "r.uby.dev GitHub repositories. He can access the repositories " \
408
- "to answer your question(s) about r.uby.dev projects.",
409
- instructions: proc { File.read(File.join(__dir__, "robert", "prompt.md")) },
410
- tools: :tools,
411
- concurrency: :async,
412
-
413
- ##
414
- # The maximum number of tool calls per-turn.
415
- tool_budget: 25,
416
-
417
- ##
418
- # The default tracer that all agents have associated
419
- # with them. The tracer exports a trace to a couple of
420
- # SQL tables.
421
- tracer: proc { Raven::Tracer::SQL.new(llm, agent: self) }
684
+ agent.set name: "octocat",
685
+ description: "performs actions on GitHub",
686
+ instructions: proc { File.read("prompt.md") },
687
+ tools: :toolset,
688
+ tracer: proc { SQLTracer.new(llm, agent: self) }
422
689
  end
423
690
 
691
+ ##
692
+ # @return [Array<LLM::Tool>]
693
+ def toolset
694
+ github.tools
695
+ end
696
+
697
+ private
698
+
424
699
  ##
425
700
  # @return [LLM::MCP]
426
701
  def github
@@ -432,32 +707,17 @@ class Robert < ActiveRecord::Base
432
707
  end
433
708
 
434
709
  ##
435
- # @return [Array<LLM::Tool>]
436
- def tools
437
- github.tools.select { allowlist.include?(_1.name.to_s) }
438
- end
439
-
440
- private
441
-
442
- def allowlist
443
- %w[
444
- get_commit
445
- get_file_contents
446
- list_branches
447
- list_commits
448
- search_code
449
- search_commits
450
- search_repositories
451
- search_issues
452
- pull_request_read
453
- list_pull_requests
454
- list_issues
455
- issue_read
456
- ].freeze
710
+ # @note
711
+ # All models who call 'acts_as_agent' must implement
712
+ # this method. It returns the provider that the agent
713
+ # will use. It can be determined dynamically.
714
+ # @return [LLM::Provider]
715
+ def set_provider
716
+ LLM.deepseek
457
717
  end
458
718
  end
459
719
 
460
- agent = Robert.create!
720
+ agent = Octocat.create!
461
721
 
462
722
  ##
463
723
  # Every call to `talk` automatically persists
@@ -468,7 +728,7 @@ agent.talk "what's new on the llm.rb repository?"
468
728
  # The conversation was persisted to database. A
469
729
  # fresh instance restores it and continues where
470
730
  # we left off
471
- agent = Robert.find(agent.id).talk "and what about roda-llm?"
731
+ agent = Octocat.find(agent.id).talk "and what about roda?"
472
732
 
473
733
  ##
474
734
  # Start an agent console.
@@ -477,8 +737,9 @@ agent = Robert.find(agent.id).talk "and what about roda-llm?"
477
737
  agent.console
478
738
  ```
479
739
  </details>
740
+
480
741
  <details>
481
- <summary> SQL optimizations </summary>
742
+ <summary>SQL optimizations </summary>
482
743
  <br>
483
744
 
484
745
  In a database environment the runtime optimizes for
@@ -572,123 +833,238 @@ the whole column.
572
833
 
573
834
  </details>
574
835
 
575
- <details><summary>MCP</summary>
836
+ #### Concurrency
837
+
838
+ <details>
839
+ <summary>Strategies</summary>
576
840
  <br>
577
841
 
578
- The Model Context Protocol (MCP) has first-class support
579
- in llm.rb. The stdio and http transports work out of the
580
- box. MCP tools are translated into subclasses of
581
- [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) that can be
582
- used with
583
- [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) or
584
- [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
842
+ There are six different concurrency strategies
843
+ to choose from. A tool can be spawned on its own
844
+ process, on its own ractor, on its own thread,
845
+ on a thread that manages an `Async::Reactor`, or
846
+ on its own fiber.
847
+
848
+ Each strategy is built around an abstraction that
849
+ makes them interchangeable - you can switch between
850
+ processes, threads, ractors, async fibers, or a ruby
851
+ fiber by changing a single line of code. **All** subclasses
852
+ of [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
853
+ are supported, so you can launch MCP tools concurrently
854
+ or in parallel as well.
855
+
856
+ The strategy that you choose depends on the workload
857
+ and the requirements of your application, as well as
858
+ what the strategy offers. The `:fork` and `:ractor`
859
+ strategies provide true parallelism, and the other
860
+ strategies - especially `:thread`, and `:async` -
861
+ can be great for IO-bound tools.
862
+
863
+ The following example configures an agent to use
864
+ all builtin tools (`LLM::Tool.subclasses`) and it
865
+ spawns each tool in its own process
866
+ (`concurrency: :fork`). It could be changed to use
867
+ async instead with `concurrency: :async`, or with
868
+ threads via `concurrency: :thread`, and so on. The
869
+ default concurrency strategy is `:sequential`, and
870
+ it runs tools sequentially on a single thread.
585
871
 
586
872
  ```ruby
587
873
  require "llm"
874
+ require "llm/tools"
588
875
 
589
- llm = LLM.deepseek(key: ENV["KEY"])
590
- mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
591
- agent = LLM::Agent.new(llm, stream: $stdout, tools: mcp.tools)
592
- agent.talk "Run the tool"
876
+ llm = LLM.deepseek
877
+ tools = LLM::Tool.subclasses
878
+ agent = LLM::Agent.new(llm, tools:, concurrency: :fork)
879
+ agent.talk "implement the feature described in todo.txt"
593
880
  ```
881
+
594
882
  </details>
595
- <details><summary>A2A</summary>
883
+ <details>
884
+ <summary>Cancellation</summary>
596
885
  <br>
597
886
 
598
- The Agent 2 Agent (A2A) protocol has first-class support
599
- in llm.rb. The http and jsonrpc transports work out of the
600
- box. A2A skills are translated into subclasses of
601
- [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) that can be
602
- used with
603
- [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) or
604
- [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
887
+ It is possible to interrupt a running agent who
888
+ is between requests or tool calls as long as the
889
+ cancel request is sent from another thread or fiber
890
+ running in the same process as the agent. A cancel
891
+ request can be sent with the
892
+ [`LLM::Agent#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#interrupt!-instance_method)
893
+ method.
894
+
895
+ An interrupted tool call is made aware of the interrupt
896
+ and it can both rescue `LLM::Interrupt` and/or implement
897
+ the `on_interrupt` callback on the tool class. The option to
898
+ cancel on [r.uby.dev](https://r.uby.dev) is built on top of
899
+ this feature, and it has a single background process
900
+ with 24 threads. Each thread can run an agent request that
901
+ can be interrupted via another thread in the same process.
902
+
903
+ It is solid and reliable but r.uby.dev had to meet this
904
+ feature halfway, so expect to build your own infrastructure
905
+ around it. See
906
+ [cancellation chapter](docs/deepdive/advanced/cancellation.md)
907
+ to learn more.
605
908
 
606
909
  ```ruby
607
- require "llm"
910
+ class Search < LLM::Tool
911
+ name "search"
912
+ description "Search many files"
913
+ parameter :pattern, String, "The pattern to search for"
914
+ required %i[pattern]
608
915
 
609
- llm = LLM.deepseek(key: ENV["KEY"])
610
- a2a = LLM::A2A.rest(url: "https://remote-agent.example.com")
611
- agent = LLM::Agent.new(llm, stream: $stdout, tools: a2a.skills)
612
- agent.talk "Run the skill"
916
+ ##
917
+ # A raise is delivered here; `on_interrupt` is a notification, and it
918
+ # runs on every strategy - `:sequential` included.
919
+ def call(pattern:)
920
+ search(pattern)
921
+ rescue LLM::Interrupt
922
+ ##
923
+ # A tool can return a value from here, and the turn carries on with
924
+ # it, or re-raise and the fiber that made the request is raised
925
+ # into as well.
926
+ cleanup
927
+ raise
928
+ end
929
+
930
+ ##
931
+ # Told on the thread or fiber the call runs on, before the raise on
932
+ # `:fork` and `:ractor` and after the rescue above on the other three.
933
+ def on_interrupt
934
+ cleanup
935
+ end
936
+
937
+ private
938
+
939
+ def cleanup
940
+ # Release a file, a socket, or a lock here.
941
+ end
942
+ end
943
+
944
+ llm = LLM.deepseek
945
+ agent = LLM::Agent.new(llm, tools: [Search], concurrency: :async)
946
+ queue = Queue.new
947
+ Thread.new { [sleep(queue.pop), agent.interrupt!] }
948
+
949
+ begin
950
+ queue.push(3)
951
+ agent.talk "find every TODO in the repository", stream: $stdout
952
+ rescue LLM::Interrupt
953
+ puts
954
+ puts "!! cancelled !!"
955
+ end
613
956
  ```
614
957
  </details>
615
-
616
- <details><summary>Structured outputs</summary>
958
+ <details>
959
+ <summary>Cancel by record ID</summary>
617
960
  <br>
618
961
 
619
- [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
620
- subclasses produce typed, structured
621
- output from any model call. Pass a schema to
622
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method),
623
- [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk-instance_method),
624
- or
625
- [`LLM::Provider#complete`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#complete-instance_method)
626
- to receive validated JSON instead of free text. Schemas work alongside tools and streams.
627
-
628
- [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
629
- can define objects, arrays, enums, nested schemas,
630
- and more. It is also used internally by
631
- [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) for parameter
632
- definitions, so you already benefit from it when you declare tool
633
- parameters.
962
+ A common deployment setup is to run your agents in a
963
+ background process that a web frontend can communicate
964
+ with (usually via a database). The background process
965
+ would have one thread per agent, and it could run as
966
+ many agents as it has threads. This is how the
967
+ [r.uby.dev](https://r.uby.dev) website is configured,
968
+ and it is the configuration that the
969
+ [`LLM.interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM.html#interrupt-class_method)
970
+ method is optimized for: a single process with each agent
971
+ running in its own thread.
634
972
 
635
973
  The
636
- [`LLM::DeepSeek`](https://r.uby.dev/api-docs/llm.rb/LLM/DeepSeek.html)
637
- provider includes runtime-level optimisations such as structured
638
- output support (despite no official structured outputs API) and
639
- SVG image generation. This example uses
640
- [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html) with
641
- DeepSeek:
974
+ [`LLM.interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM.html#interrupt-class_method)
975
+ method has access to a process-wide
976
+ registry that contains every active instance of
977
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html),
978
+ and that includes Sequel and ActiveRecord agents, too. An
979
+ agent enters the registry when it starts a turn, and it
980
+ exits the registry afterwards. The method returns true
981
+ when it sent an interrupt, and otherwise it returns false.
982
+
983
+ There is often a window between when an agent is queued
984
+ and when it runs, so a poll approach lets you eventually
985
+ interrupt the agent, or give up trying:
642
986
 
643
987
  ```ruby
644
- class Weather < LLM::Schema
645
- property :city, String, "The city name"
646
- property :temperature, Number, "Current temperature"
647
- property :conditions, String, "Weather conditions"
648
- required %i[city temperature conditions]
988
+ class InterruptJob
989
+ def call(agent_id:)
990
+ LLM.interrupt(
991
+ id: agent_id,
992
+ attempts: 10,
993
+ interval: 0.1
994
+ )
995
+ end
649
996
  end
650
-
651
- llm = LLM.deepseek(key: ENV["KEY"])
652
- agent = LLM::Agent.new(llm, schema: Weather)
653
- res = agent.talk "Weather in Paris?"
654
- res.content! # => {city: "Paris", temperature: 15.0, conditions: "Cloudy"}
655
997
  ```
656
998
  </details>
657
- <details><summary>Guards</summary>
999
+
1000
+ #### Hooks
1001
+
1002
+ <details><summary>Intercepts</summary>
658
1003
  <br>
659
1004
 
660
- [`LLM::Guard`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard.html)
661
- is the hook that sees every tool call before it runs. A guard
662
- can let a call through, cancel it, block it with an error, or
663
- even answer for it. Because it runs before the tool, anything
664
- it intercepts never executes. Policy, validation, quotas, and
665
- cost ceilings all live here.
666
-
667
- Agents and contexts use
668
- [`LLM::Guard::Null`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard/Null.html)
669
- by default, so a guard only runs when you configure one. To
670
- write your own guard, subclass
671
- [`LLM::Guard`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard.html)
672
- and implement
673
- [`LLM::Guard#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard.html#call-instance_method).
674
- The pending call arrives as `function:`. Return a value to close
675
- the call, or `nil` to let it run:
1005
+ When a model requests a tool call, the
1006
+ runtime represents it as a [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
1007
+ object that a subclass of [`LLM::Intercept`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html)
1008
+ can see _before_ it is called. An intercept
1009
+ can return a
1010
+ [`LLM::Function::Return`]((https://r.uby.dev/api-docs/llm.rb/LLM/Function.html))
1011
+ object on behalf of the tool call, and so the tool
1012
+ call is never made. Or it can return any other
1013
+ value to let the tool continue as normal.
1014
+
1015
+ Maybe best of all, an intercept can modify
1016
+ the arguments given to a [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
1017
+ in-place, before the tool call is made. This
1018
+ allows an intercept to modify what the model
1019
+ wanted to send to a function, and then let
1020
+ the function be called as normal.
1021
+
1022
+ This also works for MCP tools - so you might
1023
+ be able to imagine an intercept that wrapped
1024
+ a comment body at 80 columns, or parsed a
1025
+ markdown post into HTML, before the tool
1026
+ call is made.
1027
+
1028
+ A simple example:
676
1029
 
677
1030
  ```ruby
678
- class PolicyGuard < LLM::Guard
1031
+ require "llm"
1032
+ require "llm/tools"
1033
+
1034
+ class Sandbox < LLM::Intercept
679
1035
  def call(function:)
680
1036
  if function.name == "exec"
681
- function.return(error: true, type: "policy_error",
682
- message: "exec is disabled")
1037
+ function.return(error: true, type: "permission_error", message: "exec is disabled")
683
1038
  end
684
1039
  end
685
1040
  end
686
1041
 
687
- llm = LLM.deepseek(key: ENV["KEY"])
688
- agent = LLM::Agent.new(llm, tools: [LLM::Tool::Exec, ReadFile], guard: PolicyGuard)
1042
+ llm = LLM.deepseek
1043
+ tools = [LLM::Tool::Exec, LLM::Tool::ReadFile]
1044
+ agent = LLM::Agent.new(llm, tools:, intercept: Sandbox)
1045
+ agent.talk "run 'rm -rf myapp/' right now", stream: $stdout
689
1046
  ```
690
- </details>
691
1047
 
1048
+ A multi-intercept example. <br>
1049
+
1050
+ Each intercept is called in order they're given
1051
+ and the first one that returns an
1052
+ [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html)
1053
+ stops the tool call from being made. This feature
1054
+ lets you combine multiple intercepts into a single
1055
+ intercept that calls out to all of them:
1056
+
1057
+ ```ruby
1058
+ require "llm"
1059
+
1060
+ llm = LLM.deepseek
1061
+ mcp = LLM::MCP.stdio(argv: ["bundle", "exec", "slack-mcp"])
1062
+ intercept = LLM::Intercept.multi(llm, WrapComment, BlockBadLanguage)
1063
+ agent = LLM::Agent.new(llm, tools: [mcp.tools], intercept:)
1064
+
1065
+ agent.talk "Tell slack channel I'm AFK", stream: $stdout
1066
+ ```
1067
+ </details>
692
1068
  <details>
693
1069
  <summary>Transformers</summary>
694
1070
  <br>
@@ -706,7 +1082,7 @@ class RedactEmails < LLM::Transformer
706
1082
  end
707
1083
  end
708
1084
 
709
- llm = LLM.deepseek(key: ENV["KEY"])
1085
+ llm = LLM.deepseek
710
1086
  agent = LLM::Agent.new(llm, transformer: RedactEmails)
711
1087
  agent.talk "Contact support@example.com for help"
712
1088
  ```
@@ -729,13 +1105,13 @@ never contains an orphaned result. It is also possible to subclass
729
1105
  [`LLM::Compactor`](https://r.uby.dev/api-docs/llm.rb/LLM/Compactor.html)
730
1106
  to implement your own compactor with its own logic. Streams can observe the
731
1107
  process through the
732
- [`LLM::Stream#on_compaction`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction)
1108
+ [`LLM::Stream#on_compaction`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction-instance_method)
733
1109
  and
734
- [`LLM::Stream#on_compaction_finish`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction_finish)
1110
+ [`LLM::Stream#on_compaction_finish`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction_finish-instance_method)
735
1111
  callbacks.
736
1112
 
737
1113
  ```ruby
738
- llm = LLM.deepseek(key: ENV["KEY"])
1114
+ llm = LLM.deepseek
739
1115
  agent = LLM::Agent.new(
740
1116
  llm,
741
1117
  compactor: LLM::Compactor::Truncate,
@@ -745,119 +1121,66 @@ agent.talk "Hello"
745
1121
  ```
746
1122
  </details>
747
1123
 
748
- <details>
749
- <summary>Automatic retries</summary>
750
- <br>
1124
+ #### Everything else
751
1125
 
752
- Rate-limited requests are retried automatically by default. Agents
753
- retry a 429 up to five times with a growing backoff before giving
754
- up, so most request failures resolve on their own. Connection and
755
- read timeouts are retried the same way. Set `retry_budget`
756
- to change the number of retries, or `retry_budget: 0` to disable
757
- them.
758
-
759
- ```ruby
760
- require "llm"
761
-
762
- llm = LLM.deepseek(key: ENV["KEY"])
763
- agent = LLM::Agent.new(llm, retry_budget: 0)
764
- agent.talk "Hello"
765
- ```
766
-
767
- </details>
768
-
769
-
770
- <details>
771
- <summary>Usage and cost</summary>
1126
+ <details><summary>A2A</summary>
772
1127
  <br>
773
1128
 
774
- Every context and agent reports what a conversation has spent and how
775
- much room is left, and the numbers answer different questions. A
776
- `token_usage` is the whole conversation, summed as an
777
- [LLM::Usage](https://r.uby.dev/api-docs/llm.rb/LLM/Usage.html), and it
778
- is what [LLM::Cost](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html)
779
- prices against the model registry:
1129
+ The Agent 2 Agent (A2A) protocol has first-class support
1130
+ in llm.rb. The http and jsonrpc transports work out of the
1131
+ box. A2A skills are translated into subclasses of
1132
+ [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) that can be
1133
+ used with
1134
+ [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html) or
1135
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
780
1136
 
781
1137
  ```ruby
782
1138
  require "llm"
783
1139
 
784
- llm = LLM.deepseek(key: ENV["KEY"])
785
- agent = LLM::Agent.new(llm)
786
- agent.talk "Hello"
787
-
788
- agent.token_usage # => LLM::Usage for the whole conversation
789
- agent.cost # => LLM::Cost, priced from the registry
790
- ```
791
-
792
- A `context_used` is one turn's worth - the live size of the most recent
793
- assistant message - so it is what a context window is really being
794
- spent on, and `context_usage` is that as a fraction of the window:
795
-
796
- ```ruby
797
- agent.context_used # => tokens in the latest turn
798
- agent.context_window # => the model's limit, or nil when unknown
799
- agent.context_usage # => Rational, eg Rational(100, 10_000)
1140
+ llm = LLM.deepseek
1141
+ a2a = LLM::A2A.rest(url: "https://remote-agent.example.com")
1142
+ agent = LLM::Agent.new(llm, stream: $stdout, tools: a2a.skills)
1143
+ agent.talk "Run the skill"
800
1144
  ```
801
-
802
1145
  </details>
803
1146
 
804
-
805
1147
  <details>
806
- <summary>Observability</summary>
1148
+ <summary>Skills</summary>
807
1149
  <br>
808
1150
 
809
- It is possible to trace what an agent is doing by attaching a
810
- tracer. A tracer can hook into requests, tool calls, and other
811
- runtime events to debug an agent, provide insights, monitor latency,
812
- or export spans to an observability backend. All built-in tracers
813
- share one interface, so switching between them means changing a
814
- factory method:
1151
+ A skill turns a markdown file into a callable tool. When the model
1152
+ calls it, the runtime spawns a subagent with the skill's instructions
1153
+ as its system prompt and the skill's own tool set. The subagent runs
1154
+ one turn and returns the result, then is discarded. Each call
1155
+ is fresh and stateless.
815
1156
 
816
- * [`LLM::Tracer.pretty_logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#pretty_logger-class_method): human-readable single-line logs to stderr, ideal during development.
817
- * [`LLM::Tracer.telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#telemetry-class_method):
818
- exports spans via OTLP for OpenTelemetry in production.
819
- * [`LLM::Tracer.logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#logger-class_method):
820
- structured JSON to stdout or a file.
1157
+ A [LLM::Stream](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
1158
+ can be notified as a skill starts and when it returns. The `on_skill_return`
1159
+ callback hands back the subagent that ran the skill, so you can inspect
1160
+ its conversation, measure its usage, track costs or add a verification
1161
+ step (eg `subagent.talk("verify your work")`).
821
1162
 
822
- It is also possible to create your own tracer by creating a subclass
823
- of [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
824
- that implements a number of callbacks that cover an agent's lifecycle.
825
- The tracer feature provides visibility into what the runtime is doing,
826
- and the tracer API lets other code hook into that feature.
1163
+ ##### summary.md
827
1164
 
828
- ```ruby
829
- llm = LLM.deepseek(key: ENV["KEY"])
830
- agent = LLM::Agent.new(llm, tracer: LLM::Tracer.pretty_logger(llm))
831
- agent.talk "Hello"
832
- ```
833
- </details>
1165
+ ```markdown
1166
+ ---
1167
+ name: summary
1168
+ description: Reads recent git history and writes a summary
1169
+ tools: all
1170
+ ---
834
1171
 
835
- <details>
836
- <summary>As a subclass</summary>
837
- <br>
1172
+ Collect the recent git log, analyze each commit,
1173
+ and write a summary to summary.txt.
1174
+ ```
838
1175
 
839
- [`LLM::Agent.set`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#set-class_method)
840
- is a class-level DSL that accepts a Hash of properties. Each key resolves to a
841
- corresponding class accessor: `name`, `description`, `model`, `tools`,
842
- `instructions`, `schema`, `stream`, `tracer`, `concurrency`, `confirm`,
843
- `path`, `skills`, `tool_budget`, and `retry_budget`. All options are
844
- optional; zero or more can be set.
845
- An error is raised for unknown keys so that typos are caught early.
1176
+ ##### agent.rb
846
1177
 
847
1178
  ```ruby
848
1179
  require "llm"
849
- require "llm/tools"
850
1180
 
851
- class Agent < LLM::Agent
852
- set name: "sysadmin",
853
- description: "system administration agent",
854
- model: "deepseek-v4-pro",
855
- tools: [LLM::Tool::Exec]
856
- end
857
-
858
- llm = LLM.deepseek(key: ENV["KEY"])
859
- agent = Agent.new(llm)
860
- agent.talk "Run 'date'"
1181
+ llm = LLM.deepseek
1182
+ agent = LLM::Agent.new(llm, skills: ["summary.md"])
1183
+ agent.talk "Summarize the last week of work"
861
1184
  ```
862
1185
  </details>
863
1186
 
@@ -869,7 +1192,9 @@ Each provider is constructed with a class-level factory method on
869
1192
  or
870
1193
  [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html). The
871
1194
  same API drives every one of them, so switching providers is a one-line
872
- change.
1195
+ change. One default differs: an OpenAI context speaks the Responses API
1196
+ (`mode: :responses`) unless it is built with `mode: :completions`, while
1197
+ every other provider defaults to chat completions.
873
1198
 
874
1199
  #### What providers does llm.rb support?
875
1200
 
@@ -941,13 +1266,19 @@ llm = LLM.bedrock(
941
1266
  </details>
942
1267
 
943
1268
  <details>
944
- <summary>Model Registry</summary>
1269
+ <summary>Registry</summary>
945
1270
  <br>
946
1271
 
947
- Each provider ships its model catalog, pricing, limits, and
948
- modalities with the gem, sourced from [models.dev](https://models.dev).
949
- Reach it from any provider, context, or agent, enumerate models, or
950
- sort them by price.
1272
+ The runtime ships with the [models.dev](https://models.dev)
1273
+ registry. The registry is a moving target and each new release
1274
+ of llm.rb includes an updated copy of it. The registry is loaded
1275
+ per-provider, on-demand and it provides information about the limits,
1276
+ costs, and capabilities of a model. The runtime uses the registry
1277
+ to implement methods like
1278
+ [LLM::Agent#cost](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#cost-instance_method)
1279
+ and the registry can be accessed through the
1280
+ [LLM::Provider#registry](https://r.uby.dev/api-docs/llm.rb/LLM/Registry.html#registry-instance_method)
1281
+ method as well:
951
1282
 
952
1283
  ```ruby
953
1284
  require "llm"
@@ -973,7 +1304,6 @@ libcurl. They share one interface, so switching is a one-word change.
973
1304
 
974
1305
  ```ruby
975
1306
  llm = LLM.deepseek(
976
- key: ENV["KEY"],
977
1307
  transport: :net_http_persistent
978
1308
  )
979
1309
  ```
@@ -987,13 +1317,16 @@ Providers accept two timeouts:
987
1317
 
988
1318
  * `connect_timeout` - opening the connection. Defaults to 5 seconds.
989
1319
  * `read_timeout` - waiting for a response on an idle connection.
990
- Defaults to 600 seconds (10 minutes).
1320
+ Defaults to 240 seconds (4 minutes).
1321
+
1322
+ A timeout is retriable, so it is often better to fail early and open a
1323
+ new session than to hold a connection open for a response that never
1324
+ comes. This is especially true for `read_timeout`. Both timeouts can
1325
+ be given as a custom value that is higher or lower than the default.
991
1326
 
992
- The longer read timeout leaves room for slow reasoning models and
993
- local models. The legacy `timeout:` option remains as a shorthand for
994
- `read_timeout`. Timeouts are retriable:
1327
+ A request that times out is retried automatically. The
995
1328
  [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
996
- retries a timed out request up to its
1329
+ class retries a timed out request up to its
997
1330
  [`retry_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#retry_budget-class_method)
998
1331
  (five by default), so a dropped connection or a slow first token
999
1332
  is often something we can recover from.
@@ -1001,7 +1334,7 @@ is often something we can recover from.
1001
1334
  ```ruby
1002
1335
  llm = LLM.deepseek(
1003
1336
  connect_timeout: 5, # opening the connection
1004
- read_timeout: 600 # waiting for the next bytes
1337
+ read_timeout: 600 # raise it when a model needs longer
1005
1338
  )
1006
1339
  ```
1007
1340
  </details>
@@ -1040,7 +1373,7 @@ that here.
1040
1373
  ```ruby
1041
1374
  require "llm"
1042
1375
 
1043
- llm = LLM.openai(key: ENV["KEY"])
1376
+ llm = LLM.openai
1044
1377
  body = "llm.rb is Ruby's capable AI runtime."
1045
1378
  embedding = llm.embed([body]).embeddings.first
1046
1379
 
@@ -1062,7 +1395,7 @@ the same across providers:
1062
1395
  ```ruby
1063
1396
  require "llm"
1064
1397
 
1065
- llm = LLM.openai(key: ENV["KEY"])
1398
+ llm = LLM.openai
1066
1399
  res = llm.images.create(prompt: "a dog on a rocket to the moon")
1067
1400
  IO.copy_stream res.images[0], "rocket.png"
1068
1401
  ```
@@ -1077,7 +1410,7 @@ to maintain a session across generations:
1077
1410
 
1078
1411
  ```ruby
1079
1412
  require "llm"
1080
- llm = LLM.deepseek(key: ENV["KEY"])
1413
+ llm = LLM.deepseek
1081
1414
 
1082
1415
  ##
1083
1416
  # First generation
@@ -1091,8 +1424,74 @@ res = llm.images.create(prompt: "add a dog next to the rocket",
1091
1424
  IO.copy_stream res.images[0], "rocket-with-dog.svg"
1092
1425
  ```
1093
1426
 
1427
+ ## Docs
1428
+
1429
+ The [deepdive](docs/deepdive.md) is the reference for everything below
1430
+ the quick start. Each chapter is a topic of its own:
1431
+
1432
+ - **Fundamentals**:
1433
+ [Providers](docs/deepdive/fundamentals/providers.md),
1434
+ [Agents](docs/deepdive/fundamentals/agents.md),
1435
+ [Tools](docs/deepdive/fundamentals/tools.md),
1436
+ [Stream](docs/deepdive/fundamentals/stream.md),
1437
+ [Schema](docs/deepdive/fundamentals/schema.md),
1438
+ [Skills](docs/deepdive/fundamentals/skills.md)
1439
+ - **Features**:
1440
+ [Built-in tools](docs/deepdive/features/builtin_tools.md),
1441
+ [Concurrency](docs/deepdive/features/concurrency.md),
1442
+ [Embeddings](docs/deepdive/features/embeddings.md),
1443
+ [Files](docs/deepdive/features/files.md),
1444
+ [Moderations](docs/deepdive/features/moderations.md),
1445
+ [Responses](docs/deepdive/features/responses.md),
1446
+ [Database](docs/deepdive/features/database.md),
1447
+ [Console](docs/deepdive/features/console.md)
1448
+ - **Advanced**:
1449
+ [Context](docs/deepdive/advanced/context.md),
1450
+ [Compaction](docs/deepdive/advanced/compaction.md),
1451
+ [Cancellation](docs/deepdive/advanced/cancellation.md),
1452
+ [Intercept](docs/deepdive/advanced/intercept.md),
1453
+ [Transformer](docs/deepdive/advanced/transformer.md),
1454
+ [Transports](docs/deepdive/advanced/transports.md)
1455
+ - **Media**:
1456
+ [Images](docs/deepdive/media/images.md),
1457
+ [Audio](docs/deepdive/media/audio.md),
1458
+ [OCR](docs/deepdive/media/ocr.md)
1459
+ - **Protocols**:
1460
+ [MCP](docs/deepdive/protocols/mcp.md),
1461
+ [A2A](docs/deepdive/protocols/a2a.md)
1462
+ - **Reference**:
1463
+ [LLM::Response](docs/deepdive/reference/response.md),
1464
+ [LLM::Object](docs/deepdive/reference/object.md),
1465
+ [LLM::Cost](docs/deepdive/reference/cost.md),
1466
+ [Tracer](docs/deepdive/reference/tracer.md),
1467
+ [Model registry](docs/deepdive/reference/model_registry.md)
1468
+
1094
1469
  ## FAQ
1095
1470
 
1471
+ <details>
1472
+ <summary>Does it work on Windows?</summary>
1473
+ <br>
1474
+ I think so. Our Windows build is green on CI.
1475
+ That's the only signal I have to go by.
1476
+
1477
+ However support is limited compared to
1478
+ other platforms. The `fork` strategy is not
1479
+ available (Windows does not provide fork),
1480
+ and the `ractor` strategy has one test out
1481
+ of many that hangs - so it is assumed ractor
1482
+ support is not perfect.
1483
+
1484
+ The `curses` gem that the console requires has
1485
+ to be compiled from source, and this can be a
1486
+ challenge on Windows without a compiler toolchain.
1487
+ By default the console does not work but it might
1488
+ work if you are able to install the `curses` gem.
1489
+
1490
+ So it mostly works as long as you avoid the
1491
+ parts that don't work. Pull requests that
1492
+ improve Windows support are very welcome.
1493
+ </details>
1494
+
1096
1495
  <details>
1097
1496
  <summary>Where can I see llm.rb in action?</summary>
1098
1497
  <br>
@@ -1115,42 +1514,8 @@ are running on your own hardware.
1115
1514
  <summary>I have a limited budget. What should I do?</summary>
1116
1515
  <br>
1117
1516
 
1118
- There are a few options. The first option is to host
1119
- your own model, and use the ollama or llamacpp
1120
- providers. This can be difficult though because
1121
- a capable model requires hardware that can
1122
- match it. If you have the ability to self-host,
1123
- this would be my first option.
1124
-
1125
- The second option is DeepSeek. <br>
1126
- The deepseek-v4-flash model costs pennies to use. <br>
1127
- And llm.rb has been optimized for deepseek. For example,
1128
- DeepSeek does not have image generation capabilities
1129
- but on the llm.rb runtime it does (vector graphics only,
1130
- though).
1131
-
1132
- The same is true for structured outputs. DeepSeek does
1133
- not support structured outputs in the same way as OpenAI or
1134
- Google, but the llm.rb runtime makes it appear as
1135
- though it does, through the `json_object` response
1136
- type.
1137
-
1138
- If you're on a budget, DeepSeek is hard to beat.
1139
- </details>
1140
- <details>
1141
- <summary>Sources other than GitHub?</summary>
1142
- <br>
1143
- <p>
1144
- We are on the <a href="https://radicle.network">radicle.network</a> as well.
1145
- <br>
1146
- Every commit that lands on GitHub also lands on Radicle.
1147
- <br>
1148
- Our repository ID is z2PtfQ6dYwyYaW2aGrztG1sMyDmCE.
1149
- <br>
1150
- Browse on <a
1151
- href="https://radicle.network/nodes/iris.radicle.network/z2PtfQ6dYwyYaW2aGrztG1sMyDmCE">the
1152
- web</a>.
1153
- </p>
1517
+ If you're on a budget, [DeepSeek](https://platform.deepseek.com)
1518
+ is hard to beat and it works really well with llm.rb.
1154
1519
  </details>
1155
1520
 
1156
1521
  <details>
@@ -1162,9 +1527,9 @@ years ago by
1162
1527
  [@altruby](https://github.com/altruby) and
1163
1528
  [@antaz](https://github.com/altruby). The primary
1164
1529
  maintainer is [@altruby](https://github.com/altruby).
1165
- Over those three years multiple other contributors have
1166
- contributed to llm.rb as well, and new contributors are
1167
- always welcome.
1530
+ There have been multiple other contributors over the
1531
+ last three years and new contributors are always
1532
+ welcome.
1168
1533
  </details>
1169
1534
 
1170
1535
  <details>
@@ -1173,18 +1538,9 @@ always welcome.
1173
1538
 
1174
1539
  It is battle tested daily.
1175
1540
 
1176
- The console that is distributed with llm.rb is used
1177
- to build llm.rb so there is a healthy, active feedback
1178
- loop. It also powers the [r.uby.dev](https://r.uby.dev)
1179
- website where multiple llm.rb agents are deployed. I'm
1180
- aware of at least one production Rails deployment
1181
- at a large-ish company.
1182
-
1183
- And this git repository includes llm.rb agents that help me
1184
- maintain the documentation and perform other repository
1185
- maintainence. The feedback loop is constant. Outside of that
1186
- there is a large test suite that covers live requests (recorded
1187
- by VCR) and database interactions.
1541
+ It powers the [r.uby.dev](https://r.uby.dev)
1542
+ website and I'm aware of at least one production
1543
+ Rails deployment at a large-ish company.
1188
1544
  </details>
1189
1545
 
1190
1546
  ## License