llm.rb 15.1.0 → 15.2.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -11
  3. data/README.md +68 -43
  4. data/bin/llm.rb +12 -4
  5. data/data/alibaba.json +912 -823
  6. data/data/anthropic.json +234 -187
  7. data/data/bedrock.json +3611 -2217
  8. data/data/deepinfra.json +1164 -989
  9. data/data/deepseek.json +65 -81
  10. data/data/google.json +668 -666
  11. data/data/mistral.json +501 -460
  12. data/data/moonshot.json +43 -248
  13. data/data/openai.json +1008 -914
  14. data/data/openrouter.json +7798 -7661
  15. data/data/xai.json +194 -194
  16. data/data/zai.json +242 -149
  17. data/docs/deepdive/advanced/compaction.md +5 -5
  18. data/docs/deepdive/advanced/guard.md +2 -2
  19. data/docs/deepdive/features/builtin_tools.md +93 -22
  20. data/docs/deepdive/features/{repl.md → console.md} +28 -28
  21. data/docs/deepdive/features/database.md +3 -3
  22. data/docs/deepdive/fundamentals/agents.md +2 -2
  23. data/docs/deepdive/fundamentals/providers.md +91 -6
  24. data/docs/deepdive/fundamentals/skills.md +14 -6
  25. data/docs/deepdive/fundamentals/tools.md +63 -31
  26. data/docs/deepdive/reference/cost.md +2 -2
  27. data/docs/deepdive/reference/model_registry.md +2 -2
  28. data/docs/deepdive/reference/tracer.md +15 -13
  29. data/docs/deepdive.md +2 -2
  30. data/lib/llm/active_record/acts_as_agent.rb +9 -5
  31. data/lib/llm/agent.rb +34 -15
  32. data/lib/llm/{repl → console}/bar.rb +3 -3
  33. data/lib/llm/{repl → console}/buffer.rb +4 -4
  34. data/lib/llm/{repl → console}/color.rb +2 -2
  35. data/lib/llm/{repl → console}/command.rb +12 -12
  36. data/lib/llm/{repl → console}/commands/exit.rb +4 -4
  37. data/lib/llm/{repl → console}/commands/help.rb +1 -1
  38. data/lib/llm/{repl/commands/compact.rb → console/commands/keep.rb} +11 -9
  39. data/lib/llm/{repl → console}/commands/model.rb +2 -2
  40. data/lib/llm/{repl → console}/input/cache.rb +2 -2
  41. data/lib/llm/{repl → console}/input/char.rb +2 -2
  42. data/lib/llm/{repl → console}/input/row.rb +1 -1
  43. data/lib/llm/{repl → console}/input.rb +13 -6
  44. data/lib/llm/console/markdown/parser.rb +78 -0
  45. data/lib/llm/{repl → console}/markdown/table.rb +3 -3
  46. data/lib/llm/{repl → console}/markdown.rb +13 -30
  47. data/lib/llm/{repl → console}/node.rb +3 -3
  48. data/lib/llm/{repl → console}/status.rb +11 -11
  49. data/lib/llm/{repl → console}/stream.rb +9 -9
  50. data/lib/llm/{repl → console}/walker.rb +1 -1
  51. data/lib/llm/{repl → console}/window.rb +10 -10
  52. data/lib/llm/{repl.rb → console.rb} +38 -19
  53. data/lib/llm/context/deserializer.rb +2 -1
  54. data/lib/llm/context.rb +1 -0
  55. data/lib/llm/function/async/reactor.rb +20 -1
  56. data/lib/llm/function.rb +1 -1
  57. data/lib/llm/json_adapter.rb +40 -28
  58. data/lib/llm/message.rb +7 -0
  59. data/lib/llm/provider.rb +2 -2
  60. data/lib/llm/providers/alibaba.rb +1 -1
  61. data/lib/llm/providers/deepseek.rb +1 -1
  62. data/lib/llm/providers/openai.rb +1 -0
  63. data/lib/llm/schema/leaf.rb +34 -2
  64. data/lib/llm/schema.rb +4 -2
  65. data/lib/llm/sequel/agent.rb +9 -5
  66. data/lib/llm/tool/param.rb +5 -1
  67. data/lib/llm/tool.rb +5 -0
  68. data/lib/llm/tools/bundle.rb +53 -0
  69. data/lib/llm/tools/edit-file.rb +7 -2
  70. data/lib/llm/tools/exec.rb +78 -0
  71. data/lib/llm/tools/git.rb +27 -26
  72. data/lib/llm/tools/mkdir.rb +12 -19
  73. data/lib/llm/tools/read_file.rb +69 -9
  74. data/lib/llm/tools/rg.rb +20 -24
  75. data/lib/llm/tools/ruby.rb +17 -25
  76. data/lib/llm/tools/utils.rb +74 -1
  77. data/lib/llm/tools/write_file.rb +4 -1
  78. data/lib/llm/tracer/logger.rb +2 -2
  79. data/lib/llm/tracer/pretty_logger.rb +4 -4
  80. data/lib/llm/tracer/telemetry.rb +2 -2
  81. data/lib/llm/tracer.rb +33 -0
  82. data/lib/llm/transport/utils.rb +1 -1
  83. data/lib/llm/version.rb +1 -1
  84. data/lib/llm.rb +4 -13
  85. data/llm.gemspec +7 -8
  86. metadata +64 -37
  87. data/lib/llm/tools/shell.rb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b496d3aec5a309af0ff9eaac32cabb6c7191bf2bac1f3b664c02a56c593402e
4
- data.tar.gz: 0ae83d7794b4dad881de7b9866b3894746efa1853e349fc5707416e367802db5
3
+ metadata.gz: 5c55a3fa1151afad5720fc33bbe60a71feed5577242e53b4e8d4e2f9962a18ed
4
+ data.tar.gz: 3099ef73435ed6d91b003a40e2e5c195a82a8f631418b21e7c8cbce9088a5c76
5
5
  SHA512:
6
- metadata.gz: 9f890dd0720d0acbc79f9df2196f3595a68cd618fd0d09dc02fcab9a6fe27758ae72efd4b764614d5511bdb7914f586fba7194b3d1a5bec186562b9196753783
7
- data.tar.gz: f25bc47b9bd5dcb0afea4f2ddbcb1ad37f74863cff962de6ceefa6157af073997589fd08fad0b2df11288aa24db41685127542b4d1748a71c4fef607d6b76ba2
6
+ metadata.gz: 8e0d5dd6c98355412a31dcb638081ea9f0553725cbf30aa6598eed5d16cd6d4693b5edfae8c063625ee2f5198b342e9b11850897370404267d291ddae4d747fe
7
+ data.tar.gz: 25102554a840fee5046804d17ff91673793603b96ab9ae246a30b7d3192a792e9bd7e384d76ce20abf2d504910330202cb2d4aef0057c7875c1ce3866ccfe177
data/CHANGELOG.md CHANGED
@@ -11,10 +11,295 @@
11
11
  </p>
12
12
 
13
13
  > Changelog <br>
14
- > a [r.uby.dev](https://r.uby.dev) project
14
+ > [r.uby.dev](https://r.uby.dev) project
15
15
 
16
16
  ## What's next
17
17
 
18
+ *No unreleased changes yet. Check back after the next release.*
19
+
20
+ ## v15.2.0
21
+
22
+ Changes since `v15.1.0`.
23
+
24
+ This release renames the REPL to `LLM::Console` (with `/keep` replacing
25
+ `/compact`) and routes every shell-out tool through a shared, bounded
26
+ `exec` runner. It also adds `LLM::Message#created_at`, the `LLM::Tracer`
27
+ factory methods, a `bundle` tool, per-tool `max_bytes` output limits,
28
+ and a `-v` switch to the CLI, and refreshes the model registry.
29
+
30
+ ### Core
31
+
32
+ * **message: add `LLM::Message#created_at`** <br>
33
+ [`LLM::Message#created_at`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html#created_at-instance_method)
34
+ returns the time the message was created, defaulting to the moment the
35
+ message is initialized. The timestamp is serialized into
36
+ [`LLM::Context#to_json`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#to_json-instance_method)
37
+ as an ISO-8601 string and restored on deserialization, so it can be
38
+ stored alongside the rest of the conversation.
39
+
40
+ ### Agent
41
+
42
+ * **agent: inherit the ORM model's name** <br>
43
+ An `acts_as_agent` (ActiveRecord) or `plugin :agent` (Sequel) model now
44
+ names its generated
45
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html) after
46
+ the model class. Previously the agent was an anonymous subclass, so
47
+ without an explicit name it defaulted to a gibberish `#<Class:0x...>`
48
+ string. The wrapper now initializes the agent's name before `.agent`
49
+ returns, and
50
+ [`LLM::Agent.name`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#name-class_method)
51
+ kebab-cases a `Class` argument, so an `AdminUser` model yields an agent
52
+ named `admin-user`.
53
+
54
+ ### Cli
55
+
56
+ * **cli: add a `-v` switch** <br>
57
+ `bin/llm.rb` gains a `-v` switch that prints the version (`llm.rb v#{LLM::VERSION}`) and exits.
58
+
59
+ ### Console
60
+
61
+ * **console: raise `LLM::Interrupt` on the agent's thread** <br>
62
+ Pressing Esc to cancel now also raises `LLM::Interrupt` on the agent's
63
+ thread. `LLM::Agent#cancel!` alone can be a no-op at some stages of the
64
+ request lifecycle, so the console backs it up by interrupting the thread
65
+ that runs the agent.
66
+
67
+ * **console: add a `/keep` command and retire `/compact`** <br>
68
+ The console now offers `/keep` for freeing space in the context window;
69
+ the `/compact` command is removed. `/keep` takes the same argument, so
70
+ `/keep 20%` keeps 20% of the context window. Closes
71
+ [issue #161](https://github.com/r-uby-dev/llm.rb/issues/161).
72
+
73
+ * **console: keep the UI responsive during long streams** <br>
74
+ A model can emit many chunks in a single turn. The console now draws
75
+ at most four streamed chunks at a time, then checks for input, so the
76
+ UI stays responsive even when a turn produces a large amount of
77
+ output.
78
+
79
+ * **console: persist the conversation when a turn is done** <br>
80
+ The console now saves the agent's state after the turn finishes,
81
+ rather than while the response is still streaming. State is still
82
+ saved every turn, but not until the turn has completed.
83
+
84
+ * **console: render markdown text as typed** <br>
85
+ Fix a bug where [`LLM::Console::Markdown`](https://r.uby.dev/api-docs/llm.rb/LLM/Console/Markdown.html)
86
+ mangled the model's output: HTML could render invisible, and
87
+ sequences like `...` were converted to unicode glyphs. The renderer
88
+ now uses a custom kramdown parser that disables the HTML, smart-quote,
89
+ and typographic-symbol parsers, so tags and punctuation come through
90
+ exactly as written.
91
+
92
+ * **console: fix a crash in the markdown parser** <br>
93
+ Fix a bug where the markdown renderer raised an error on an unclosed
94
+ HTML tag or a partial tag taken out of context, such as `4 < 5`. The
95
+ parser now emits the `<...` run literally when there is no closing
96
+ `>`, so the text renders instead of crashing.
97
+
98
+ * **console: stop rendering bare pipes as tables** <br>
99
+ Fix a bug where the markdown renderer treated a lone `|foo|` in prose
100
+ as a table and mangled its output. A pipe line now parses as a table
101
+ only when a header row is followed by a delimiter row, so bare pipes
102
+ come through literally while real tables still render.
103
+
104
+ * **console: find the worker thread when cancelling** <br>
105
+ Fix a bug where pressing Esc to cancel raised `LLM::Interrupt` on an
106
+ instance variable that does not exist, so the interrupt was a no-op and
107
+ a cancel could leave the turn running. The console now resolves the
108
+ worker thread through its `#thread` reader and interrupts it.
109
+
110
+ * **console: protect the state write from cancellation** <br>
111
+ The console now defers `LLM::Interrupt` while it saves the agent's
112
+ state after a turn, so a cancel that arrives during the write cannot
113
+ interrupt `agent.save` mid-flight and risk a lost or corrupted session
114
+ file.
115
+
116
+ ### Tools
117
+
118
+ * **tools: the command runner is now `exec`** <br>
119
+ The command tool that spawns a process without a shell is now
120
+ [`LLM::Tool::Exec`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Exec.html),
121
+ with the tool name `exec` instead of the previous `shell`. This is an
122
+ internal refactor of the shell-out tools: `git`, `rg`, `mkdir`,
123
+ `ruby`, and `bundle` all route through it and inherit
124
+ its bounded output.
125
+
126
+ * **tools: report when a command cannot be found** <br>
127
+ `LLM::Tool::Exec` now returns `{ok: false, error: "command 'NAME' was
128
+ not found on this system"}` when the requested command is missing,
129
+ instead of a bare `{ok: false}` result that did not tell the model why
130
+ the tool failed.
131
+
132
+ * **tools: drop the `name:` parameter from `LLM::Tool::Exec#call`** <br>
133
+ `LLM::Tool::Exec#call` now takes a single `arguments:` array instead of
134
+ separate `name:` and `arguments:` parameters, with the command name as
135
+ the first element (for example `arguments: ["rg", "-m", "10", "lib"]`).
136
+ The `Git`, `Mkdir`, `Rg`, `Ruby`, and `Bundle` tools build their calls
137
+ the same way. The change was made after models were observed confusing
138
+ the two parameters, so a single list is simpler and more reliable.
139
+
140
+ * **tools: rename `repl` as `console`** <br>
141
+ The interactive loop is renamed to
142
+ [`LLM::Console`](https://r.uby.dev/api-docs/llm.rb/LLM/Console.html),
143
+ which better reflects what it does. `agent.console` is the primary
144
+ entry point, and the require path moves from `llm/repl` to
145
+ `llm/console`. Backwards-compatible aliases remain: `LLM::Repl`,
146
+ `LLM::Agent#repl`, the ORM wrappers' `#repl`, and `LLM::Command =`
147
+ `LLM::Console::Command`.
148
+
149
+ * **tools: `LLM::Tool::Git#call` takes an `arguments:` array** <br>
150
+ [`LLM::Tool::Git#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Git.html)
151
+ now takes a single `arguments:` array in place of the previous
152
+ `subcommand:` parameter. The first element must be one of `log`,
153
+ `diff`, `commit`, `checkout`, `branch`, or `show`, validated before the
154
+ command is spawned; the remaining elements are forwarded to git.
155
+
156
+ * **tools: `LLM::Tool::Utils` now owns command spawning** <br>
157
+ The shared [`LLM::Tool::Utils`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Utils.html)
158
+ module now requires the `test-cmd.rb` gem (at `~> 2.7.1`) itself and
159
+ exposes the `spawn` and `wait` helpers, so any tool that includes
160
+ `Utils` gets command spawning without requiring `exec` directly. The
161
+ `Git`, `Mkdir`, `Rg`, `Ruby`, `Exec`, and `Bundle` tools all
162
+ inherit their bounded-output protections from this shared runner.
163
+
164
+ * **tools: route `git`, `rg`, `mkdir`, and `ruby` through `exec`** <br>
165
+ `LLM::Tool::Git`, `LLM::Tool::Rg`, `LLM::Tool::Mkdir`, and
166
+ `LLM::Tool::Ruby` now implement their calls through the `exec` tool,
167
+ completing the refactor so every tool that shells out flows through
168
+ the shared command runner with its bounded output.
169
+
170
+ * **tools: read-file returns structured lines** <br>
171
+ `LLM::Tool::ReadFile#call` now returns its content as structured
172
+ `{lineno:, content:}` lines under a `lines:` key instead of a single
173
+ `content:` string, and adds a `truncated:` flag. A reversed range
174
+ (`start: 20, stop: 2`) is swapped to read lines 2 through 20. The
175
+ truncation marker is kept out of the returned lines, so the model
176
+ does not mistake it for a real file line.
177
+
178
+ * **tools: write-file appends a trailing newline by default** <br>
179
+ `LLM::Tool::WriteFile` now ensures written content ends with a newline,
180
+ adding one when the content does not already end with `\n`. It previously
181
+ wrote the content exactly as given. A new `newline:` parameter (default
182
+ `true`) controls this, so `newline: false` writes the content exactly as
183
+ given.
184
+
185
+ * **tools: fix `edit-file` treating `before` as a regex** <br>
186
+ `LLM::Tool::EditFile` now escapes the `before` snippet with
187
+ `Regexp.escape`, so regex metacharacters are matched literally, and
188
+ switches to the block form of `sub` so the `after` replacement keeps
189
+ backslash sequences like `\1` and `\&` literal.
190
+
191
+ * **tools: bound tool output with a per-tool `max_bytes`** <br>
192
+ Each of the `Exec`, `ReadFile`, `Rg`, `Mkdir`, `Ruby`, and
193
+ `Bundle` tools gains a `max_bytes` limit (default 75,000) for the
194
+ maximum number of bytes a tool returns to the model. `Exec` and
195
+ `ReadFile` add the class-level `max_bytes` accessor, which the other
196
+ tools inherit through `Exec`, so each tool's cap can be configured
197
+ independently, for example `LLM::Tool::ReadFile.max_bytes(175_000)`.
198
+ It does not enforce the limit by itself;
199
+ [`LLM::Tool::Utils#truncate`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Utils.html#truncate-instance_method)
200
+ trims a string within the limit and marks the trailing content as
201
+ truncated, and `truncate!` returns a `[content, truncated]` tuple for
202
+ callers that structure truncated output themselves. `rg` also gains a
203
+ `max_count:` parameter that caps the number of results per file.
204
+
205
+ * **tools: add a `bundle` tool** <br>
206
+ A new [`LLM::Tool::Bundle`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Bundle.html)
207
+ tool runs a command through `bundle`. It uses the `BUNDLE_GEMFILE`
208
+ environment variable when set, or a `Gemfile` in the current working
209
+ directory otherwise. The tool takes an `arguments:` array, so the
210
+ model passes the bundle command and its arguments as a single list,
211
+ for example `arguments: ["exec", "rspec"]`.
212
+
213
+ * **tools: resolve defaults through `LLM::Utils.resolve_option`** <br>
214
+ A tool parameter default can now be an immediate value, a Symbol resolved
215
+ as a method on the tool, or a Proc evaluated lazily at runtime, matching
216
+ how `LLM::Agent` resolves its attributes. This lets a default track a
217
+ value that can change between boot and runtime, such as a tool's
218
+ `max_bytes`.
219
+
220
+ ### Registry
221
+
222
+ * **refresh model metadata** <br>
223
+ Update `data/` with current pricing, limits, and capabilities for the
224
+ OpenRouter, OpenAI, Bedrock, DeepInfra, DeepSeek, Google, Mistral,
225
+ Moonshot, Z.ai, and Alibaba registries.
226
+
227
+ ### Provider
228
+
229
+ * **provider: retry `Net::WriteTimeout`, too** <br>
230
+ Requests that raise `Net::WriteTimeout` are now retried alongside the
231
+ other timed-out and rate-limited requests, up to the `retry_budget`,
232
+ matching how `Net::OpenTimeout` and `Net::ReadTimeout` are handled. The
233
+ console status bar also reports a write timeout as `Timed out`.
234
+
235
+ * **alibaba: default to a retry budget of 8** <br>
236
+ An agent that runs on the Alibaba provider now defaults to a retry
237
+ budget of 8 instead of 5, because Alibaba (token plan) frequently rate
238
+ limits and times out requests that it later recovers from. An explicit
239
+ `retry_budget:` still overrides the default.
240
+
241
+ * **deepseek: default to the `deepseek-flash` model** <br>
242
+ The default DeepSeek chat model is now `deepseek-flash` instead of
243
+ `deepseek-v4-flash`. DeepSeek resolves `deepseek-flash` to
244
+ `deepseek-v4.1-flash` and recommends the name in its documentation and
245
+ API error messages, so the default follows the current model alias
246
+ instead of a pinned version.
247
+
248
+ ### Tracer
249
+
250
+ * **tracer: add `LLM::Tracer` factory methods** <br>
251
+ Add
252
+ [`LLM::Tracer.logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#logger-class_method),
253
+ [`LLM::Tracer.pretty_logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#pretty_logger-class_method),
254
+ and
255
+ [`LLM::Tracer.telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#telemetry-class_method)
256
+ as the preferred way to build a tracer for a provider, so switching
257
+ between tracers means changing a factory method instead of a class name.
258
+ The old `LLM.logger(llm, ...)` convenience method is removed in favor
259
+ of `LLM::Tracer.logger(llm, ...)`.
260
+
261
+ * **tracer: add `path:` support to `LLM::Tracer::PrettyLogger`** <br>
262
+ [`LLM::Tracer::PrettyLogger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/PrettyLogger.html)
263
+ now accepts a `path:` option to write its human-readable entries to a
264
+ file, matching `LLM::Tracer::Logger`. It previously only accepted `io:`.
265
+
266
+ ### Fix
267
+
268
+ * **json: scrub invalid UTF-8 on dump** <br>
269
+ Fix a bug where [`LLM::JSONAdapter`](https://r.uby.dev/api-docs/llm.rb/LLM/JSONAdapter.html)
270
+ raised a JSON generator error when dumping a string tagged as UTF-8 that
271
+ carried invalid bytes. The normalize step now transcodes every string to
272
+ valid UTF-8, replacing invalid sequences with the replacement character,
273
+ so dumping works on `json ~> 3.0`. The `oj` and `yajl` adapters now run
274
+ the same normalization, so every backend scrubs invalid bytes before
275
+ serializing.
276
+
277
+ * **fork: require xchan.rb `~> 0.23`** <br>
278
+ The `:fork` concurrency strategy now requires the `xchan.rb` gem at
279
+ `~> 0.23` instead of `~> 0.22`. xchan.rb 0.23.0 replaces the external
280
+ `lockf.rb` gem with a built-in, Fiddle-based `Chan::Lockf`, so fork
281
+ channels no longer carry that extra dependency. (The socket
282
+ length-header deadlock fix shipped earlier, in xchan.rb 0.22.0.)
283
+
284
+ * **async: fix a shutdown exception on the reactor thread** <br>
285
+ Fix a bug where the `:async` strategy's
286
+ [`LLM::Function::Async::Reactor`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Async/Reactor.html)
287
+ raised a `TypeError` on shutdown with recent `async` and `io-event`
288
+ versions, because their internals tried to raise an integer as an
289
+ exception. The scheduler is now detached from the reactor thread
290
+ before it exits, which avoids that code path entirely, and teardown
291
+ is managed by `reactor.stop`, so the thread exits promptly instead of
292
+ abruptly or hanging.
293
+
294
+ * **openai: prevent the loss of user messages in the completions path** <br>
295
+ Fix a bug where the request body was built from `params[:messages]`
296
+ alone when that key was present, discarding the messages built from the
297
+ prompt. The DeepSeek and Alibaba schema support injects the schema
298
+ system message into `params[:messages]`, so a request with a `schema:`
299
+ could be sent with the schema message only, dropping the user's
300
+ messages. The built messages now lead with `params[:messages]`, and the
301
+ key is removed before the body is assembled.
302
+
18
303
  ## v15.1.0
19
304
 
20
305
  Changes since `v15.0.3`.
@@ -23,7 +308,7 @@ This release adds the OpenRouter provider, splits provider timeouts
23
308
  into `connect_timeout` and `read_timeout`, retries timed-out requests,
24
309
  and renames `on_rate_limit` to `on_retry`. Skills now gain a
25
310
  frontmatter `model:` parameter and inherit the parent agent's model,
26
- while the CLI gains `-m` and `-x` switches, and the REPL shows retry
311
+ while the CLI gains `-m` and `-x` switches, and the console shows retry
27
312
  progress and measures text by display width.
28
313
 
29
314
  ### Provider
@@ -132,21 +417,21 @@ progress and measures text by display width.
132
417
  up to three stack lines from the backtrace, so the failure is easier to
133
418
  locate and report than a bare diagnostic.
134
419
 
135
- ### Repl
420
+ ### Console
136
421
 
137
- * **repl: show retry progress in the status bar** <br>
138
- When a request is rate limited or times out, the curses-based REPL status
422
+ * **console: show retry progress in the status bar** <br>
423
+ When a request is rate limited or times out, the curses-based console status
139
424
  bar shows a retry indicator with the error and the remaining attempts, for
140
425
  example `🔁 Rate limited • attempt 2 of 5`.
141
426
 
142
- * **repl: measure text width with `unicode-display_width`** <br>
143
- The curses-based REPL now counts and slices text by display column width
427
+ * **console: measure text width with `unicode-display_width`** <br>
428
+ The curses-based console now counts and slices text by display column width
144
429
  instead of character count, so wrapping, table columns, and clipping stay
145
430
  aligned for wide characters such as emoji. It requires the optional
146
431
  `unicode-display_width` gem.
147
432
 
148
- * **repl: treat `LLM::InsufficientQuotaError` as a rate limit in the status bar** <br>
149
- The curses-based REPL status bar now shows `Rate limited` when a request
433
+ * **console: treat `LLM::InsufficientQuotaError` as a rate limit in the status bar** <br>
434
+ The curses-based console status bar now shows `Rate limited` when a request
150
435
  raises
151
436
  [`LLM::InsufficientQuotaError`](https://r.uby.dev/api-docs/llm.rb/LLM/InsufficientQuotaError.html),
152
437
  matching how ordinary `LLM::RateLimitError`s are shown, instead of falling
@@ -1222,7 +1507,7 @@ reliable across all six concurrency backends. The `functions` and
1222
1507
  `LLM.require` now accepts a second `version` parameter that is passed
1223
1508
  to `Kernel#gem` before loading, enabling version constraints for
1224
1509
  optional runtime dependencies. For example,
1225
- `LLM.require "test-cmd.rb", "~> 2.1"` ensures a minimum gem version
1510
+ `LLM.require "test-cmd.rb", "~> 2.2"` ensures a minimum gem version
1226
1511
  is available. This is used internally by the `Git`, `Rg`, `Mkdir`,
1227
1512
  and `Shell` tools to enforce compatibility with the `test-cmd.rb` gem.
1228
1513
 
@@ -2286,7 +2571,7 @@ As always, see the changelog details for a thorough overview.
2286
2571
  * **Add `LLM::Agent#repl`** <br>
2287
2572
  Add a curses-based read-eval-print loop for `LLM::Agent` that lets
2288
2573
  developers interact with an agent after it has been set up or has
2289
- performed a task. It is similar to `binding.pry`: once you exit,
2574
+ performed a task. It is similar to `binding.irb`: once you exit,
2290
2575
  you can continue with the rest of your program. It requires the
2291
2576
  `curses` gem.
2292
2577
 
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <p align="center">
2
2
  <a href="https://r.uby.dev">
3
3
  <img
4
- src="https://github.com/r-uby-dev/llm.rb/raw/main/rubydev.svg"
4
+ src="rubydev.svg"
5
5
  width="400"
6
6
  height="200"
7
7
  border="0"
@@ -10,16 +10,14 @@
10
10
  </a>
11
11
  </p>
12
12
 
13
- > A [r.uby.dev](https://r.uby.dev/llm) project.
13
+ > [r.uby.dev](https://r.uby.dev/llm) project.
14
14
 
15
15
  Welcome to the canonical llm.rb repository.
16
16
 
17
17
  llm.rb is an advanced 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
- that spans 14+ providers. Streaming, tools, guards, compaction, the
21
- REPL, builtin MCP/A2A support and the database integrations all build
22
- on the same three concepts: providers, contexts, and agents.
20
+ that spans 14+ providers.
23
21
 
24
22
  The easiest way to learn about llm.rb is to ask [the r.uby.dev chatbot](https://r.uby.dev)
25
23
  a question. It is connected to the llm.rb GitHub repository, backed by
@@ -206,7 +204,7 @@ isolation from its parent.
206
204
 
207
205
  A couple of concurrency strategies require optional, opt-in dependencies.
208
206
  The `async` strategy requires the [async](https://github.com/socketry/async)
209
- gem and the `fork` strategy requires the [xchan.rb](https://github.com/0x1eef/xchan.rb)
207
+ gem and the `fork` strategy requires the [xchan.rb](https://github.com/r-uby-dev/xchan.rb)
210
208
  gem. The `fiber` strategy requires a scheduler (`Fiber.scheduler`) but by
211
209
  default Ruby does not provide one.
212
210
 
@@ -247,27 +245,25 @@ end
247
245
  ```
248
246
  </details>
249
247
  <details>
250
- <summary>Console (<code>binding.pry</code> for agents)</summary>
248
+ <summary>Console (<code>binding.irb</code> for agents)</summary>
251
249
  <br>
252
250
 
253
- The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
254
- method drops you into a highly capable read-eval-print loop (REPL)
251
+ The [LLM::Agent#console](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#console-instance_method)
252
+ method drops you into a highly capable interactive console
255
253
  that is built on top of curses. It can help you debug agents,
256
254
  test your tools, connect to MCP servers, and even A2A agents.
257
- The REPL stands out because it connects to the surrounding
255
+ The console stands out because it connects to the surrounding
258
256
  runtime and it can be extended by your code. Think of it as
259
- `binding.pry` but for agents.
257
+ `binding.irb` but for agents.
260
258
 
261
259
  ##### Demo
262
260
 
263
- [Watch in high quality on asciinema](https://asciinema.org/a/OsS8wwaasKasoDDz)
264
-
265
- ![llm.rb REPL demo](demo.gif)
261
+ ![llm.rb console demo](demo.gif)
266
262
 
267
263
 
268
264
  ##### Installation
269
265
 
270
- The REPL is distributed with llm.rb so you don't have to install
266
+ The console is distributed with llm.rb so you don't have to install
271
267
  a separate gem but it requires a number of optional dependencies
272
268
  to be installed separately. The following gems provide the full
273
269
  experience:
@@ -276,8 +272,8 @@ experience:
276
272
 
277
273
  ##### Persistence
278
274
 
279
- The `path:` option can be set on an agent for automatic persistence
280
- across REPL sessions. The `tools:` option attaches extra tools
275
+ the `path:` option can be set on an agent for automatic persistence
276
+ across console sessions. The `tools:` option attaches extra tools
281
277
  for the duration of the session. Recall previous turns with Ctrl+P and
282
278
  Ctrl+N.
283
279
 
@@ -287,21 +283,26 @@ require "llm/tools"
287
283
 
288
284
  llm = LLM.deepseek(key: ENV["KEY"])
289
285
  agent = LLM::Agent.new(llm, name: "my-agent", path: "agent.json")
290
- agent.repl(tools: LLM::Tool.subclasses)
286
+ agent.console(tools: LLM::Tool.subclasses)
291
287
  ```
292
288
 
293
289
  ##### CLI
294
290
 
295
291
  The `llm.rb` executable is available on your PATH after installation.
296
- It starts a REPL session from any directory.The CLI auto-detects your
292
+ It starts a console session from any directory. The CLI auto-detects your
297
293
  provider from standard environment variables (`DEEPSEEK_API_KEY`,
298
294
  `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc.). Persistent sessions are
299
295
  stored under `~/.llm.rb/` and restored automatically on your next visit.
300
296
 
301
297
  ```bash
302
- llm.rb # auto-detect from $DEEPSEEK_API_KEY
298
+ llm.rb # auto-detect from $PROVIDER_API_KEY
303
299
  llm.rb -p openai # use OpenAI explicitly
300
+ llm.rb -m gpt-5.6 # use a model other than the provider default
301
+ llm.rb -c thread # run tool calls on a separate thread
302
+ llm.rb -n curb # use libcurl as the HTTP transport
303
+ llm.rb -x 900 # read timeout of 15 minutes
304
304
  llm.rb -t # temporary session, no persistence
305
+ llm.rb -v # print the version
305
306
  ```
306
307
  </details>
307
308
  <details>
@@ -423,8 +424,8 @@ agent = Raven.find(agent.id).tap(&:research_codebase)
423
424
  ##
424
425
  # Start an agent console.
425
426
  # Query agent's state, debug, etc.
426
- # The REPL does not persist back to the database.
427
- agent.repl
427
+ # The console does not persist back to the database.
428
+ agent.console
428
429
  ```
429
430
  </details>
430
431
 
@@ -534,15 +535,15 @@ the call, or `nil` to let it run:
534
535
  ```ruby
535
536
  class PolicyGuard < LLM::Guard
536
537
  def call(function:)
537
- if function.name == "shell"
538
+ if function.name == "exec"
538
539
  function.return(error: true, type: "policy_error",
539
- message: "shell is disabled")
540
+ message: "exec is disabled")
540
541
  end
541
542
  end
542
543
  end
543
544
 
544
545
  llm = LLM.deepseek(key: ENV["KEY"])
545
- agent = LLM::Agent.new(llm, tools: [Shell, ReadFile], guard: PolicyGuard)
546
+ agent = LLM::Agent.new(llm, tools: [LLM::Tool::Exec, ReadFile], guard: PolicyGuard)
546
547
  ```
547
548
  </details>
548
549
 
@@ -629,21 +630,28 @@ agent.talk "Hello"
629
630
  <summary>Observability</summary>
630
631
  <br>
631
632
 
632
- Trace what an agent is doing by attaching a tracer. Hook into
633
- requests, tool calls, and other runtime events to debug a
634
- misbehaving agent, monitor latency, or export spans to an
635
- observability backend. All built-in tracers share one interface,
636
- so switching between them means changing a class name:
633
+ It is possible to trace what an agent is doing by attaching a
634
+ tracer. A tracer can hook into requests, tool calls, and other
635
+ runtime events to debug an agent, provide insights, monitor latency,
636
+ or export spans to an observability backend. All built-in tracers
637
+ share one interface, so switching between them means changing a
638
+ factory method:
637
639
 
638
- * [`LLM::Tracer::PrettyLogger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/PrettyLogger.html): human-readable single-line logs to stderr, ideal during development.
639
- * [`LLM::Tracer::Telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Telemetry.html):
640
+ * [`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.
641
+ * [`LLM::Tracer.telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#telemetry-class_method):
640
642
  exports spans via OTLP for OpenTelemetry in production.
641
- * [`LLM::Tracer::Logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Logger.html):
643
+ * [`LLM::Tracer.logger`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#logger-class_method):
642
644
  structured JSON to stdout or a file.
643
645
 
646
+ It is also possible to create your own tracer by creating a subclass
647
+ of [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
648
+ that implements a number of callbacks that cover an agent's lifecycle.
649
+ The tracer feature provides visibility into what the runtime is doing,
650
+ and the tracer API lets other code hook into that feature.
651
+
644
652
  ```ruby
645
653
  llm = LLM.deepseek(key: ENV["KEY"])
646
- agent = LLM::Agent.new(llm, tracer: LLM::Tracer::PrettyLogger.new(llm))
654
+ agent = LLM::Agent.new(llm, tracer: LLM::Tracer.pretty_logger(llm))
647
655
  agent.talk "Hello"
648
656
  ```
649
657
  </details>
@@ -668,7 +676,7 @@ class Agent < LLM::Agent
668
676
  set name: "sysadmin",
669
677
  description: "system administration agent",
670
678
  model: "deepseek-v4-pro",
671
- tools: [LLM::Tool::Shell]
679
+ tools: [LLM::Tool::Exec]
672
680
  end
673
681
 
674
682
  llm = LLM.deepseek(key: ENV["KEY"])
@@ -756,7 +764,7 @@ require "llm"
756
764
 
757
765
  llm = LLM.openai
758
766
  registry = llm.registry # => LLM::Provider#registry
759
- cheapest = registry.models.sort.first # => LLM::Model
767
+ cheapest = registry.models.sort.first # => LLM::Registry::Model
760
768
  cheapest.id # => "text-embedding-3-small"
761
769
  cheapest.context_window # => 8191
762
770
  cheapest.structured_output? # => false
@@ -895,6 +903,17 @@ IO.copy_stream res.images[0], "rocket-with-dog.svg"
895
903
 
896
904
  ## FAQ
897
905
 
906
+ <details>
907
+ <summary>Where can I see llm.rb in action?</summary>
908
+ <br>
909
+ <p>
910
+
911
+ The [r.uby.dev](https://r.uby.dev) website is powered
912
+ by llm.rb and its builtin MCP feature. It is connected
913
+ to this very GitHub repository. It is designed to help
914
+ you learn and troubleshoot llm.rb.
915
+ </p>
916
+ </details>
898
917
  <details>
899
918
  <summary>What about local LLM support?</summary>
900
919
  <br>
@@ -962,7 +981,7 @@ than three years and over that time multiple other
962
981
  contributors have contributed to llm.rb as well. New
963
982
  contributors are always welcome.
964
983
 
965
- I use the repl that is distributed with llm.rb to build
984
+ I use the console that is distributed with llm.rb to build
966
985
  llm.rb itself so there is a healthy feedback loop and
967
986
  llm.rb has also been battle tested in production
968
987
  environments.
@@ -975,13 +994,19 @@ I am constantly focused on improving llm.rb by using
975
994
  it as my primary driver for development.
976
995
  </details>
977
996
 
978
- ## Resources
997
+ ## See also
998
+
999
+ The [roda-llm](https://github.com/r-uby-dev/roda-llm#readme) project
1000
+ is how I deploy multiple ActiveRecord-backed llm.rb agents over HTTP.
1001
+ Each agent has an identical interface at a unique path that provide
1002
+ CRUD operations and stream support (via SSE - Server Side Events).
1003
+ It lets you focus on implementing agents rather than the glue that
1004
+ brings them together. It is implemented as a Roda plugin that could
1005
+ be hosted within a Rails application or other Rack-based applications.
979
1006
 
980
- The [r.uby.dev chatbot](https://r.uby.dev) is connected
981
- to this very GitHub repository. It can read documentation,
982
- source code, issues, and pull requests. The [docs/](docs/)
983
- directory contains the full documentation and the chatbot
984
- can find the answers to your questions there.
1007
+ The [docs/](docs/) directory contains the full documentation and
1008
+ the chatbot can find the answers to your questions there. Or you
1009
+ can read them yourself.
985
1010
 
986
1011
  ## License
987
1012
 
data/bin/llm.rb CHANGED
@@ -42,6 +42,10 @@ def wrap(text, width)
42
42
  end
43
43
  end
44
44
 
45
+ def version
46
+ warn "llm.rb v#{LLM::VERSION}"
47
+ end
48
+
45
49
  def help
46
50
  prog = File.basename($PROGRAM_NAME)
47
51
  warn ""
@@ -54,6 +58,7 @@ def help
54
58
  warn " -n TRANSPORT HTTP transports - net-http (default), net-http-persistent, and curb"
55
59
  warn " -x TIMEOUT The default read timeout (in seconds)"
56
60
  warn " -t Temporary session that doesn't persist to disk"
61
+ warn " -v Print version information"
57
62
  warn " -h Show this help"
58
63
  warn ""
59
64
  warn "Examples:"
@@ -74,12 +79,12 @@ def loaderror(ex)
74
79
  warn ""
75
80
  warn_title "Missing dependency: #{gem}"
76
81
  warn ""
77
- warn " The repl needs this gem, but it's not installed."
82
+ warn " The console needs this gem, but it's not installed."
78
83
  warn ""
79
84
  wrapped "Fix: gem install #{gem}", " "
80
85
  wrapped "Or: bundle add #{gem}", " "
81
86
  warn ""
82
- warn " Tip: If you don't need the repl, you can use the"
87
+ warn " Tip: If you don't need the console, you can use the"
83
88
  wrapped "library directly with: require \"llm\"", " "
84
89
  warn ""
85
90
  warn " ───────────────────────────────────────────────────────"
@@ -120,7 +125,7 @@ def main(argv)
120
125
  # Make sure the dependencies are satisified first
121
126
  begin
122
127
  require "llm/tools"
123
- require "llm/repl"
128
+ require "llm/console"
124
129
  rescue LLM::LoadError => ex
125
130
  loaderror(ex)
126
131
  exit 1
@@ -135,6 +140,9 @@ def main(argv)
135
140
  when '-h'
136
141
  help
137
142
  exit 0
143
+ when '-v'
144
+ version
145
+ exit 0
138
146
  when '-t'
139
147
  temp = true
140
148
  when '-c'
@@ -249,7 +257,7 @@ def main(argv)
249
257
  concurrency ||= :sequential
250
258
  path = temp ? nil : data[Dir.getwd]
251
259
  agent = LLM::Agent.new(llm, model:, path:, concurrency:, tools: LLM::Tool.subclasses)
252
- agent.repl
260
+ agent.console
253
261
  rescue Interrupt
254
262
  warn "llm.rb: Bye!"
255
263
  rescue => ex