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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +296 -11
- data/README.md +68 -43
- data/bin/llm.rb +12 -4
- data/data/alibaba.json +912 -823
- data/data/anthropic.json +234 -187
- data/data/bedrock.json +3611 -2217
- data/data/deepinfra.json +1164 -989
- data/data/deepseek.json +65 -81
- data/data/google.json +668 -666
- data/data/mistral.json +501 -460
- data/data/moonshot.json +43 -248
- data/data/openai.json +1008 -914
- data/data/openrouter.json +7798 -7661
- data/data/xai.json +194 -194
- data/data/zai.json +242 -149
- data/docs/deepdive/advanced/compaction.md +5 -5
- data/docs/deepdive/advanced/guard.md +2 -2
- data/docs/deepdive/features/builtin_tools.md +93 -22
- data/docs/deepdive/features/{repl.md → console.md} +28 -28
- data/docs/deepdive/features/database.md +3 -3
- data/docs/deepdive/fundamentals/agents.md +2 -2
- data/docs/deepdive/fundamentals/providers.md +91 -6
- data/docs/deepdive/fundamentals/skills.md +14 -6
- data/docs/deepdive/fundamentals/tools.md +63 -31
- data/docs/deepdive/reference/cost.md +2 -2
- data/docs/deepdive/reference/model_registry.md +2 -2
- data/docs/deepdive/reference/tracer.md +15 -13
- data/docs/deepdive.md +2 -2
- data/lib/llm/active_record/acts_as_agent.rb +9 -5
- data/lib/llm/agent.rb +34 -15
- data/lib/llm/{repl → console}/bar.rb +3 -3
- data/lib/llm/{repl → console}/buffer.rb +4 -4
- data/lib/llm/{repl → console}/color.rb +2 -2
- data/lib/llm/{repl → console}/command.rb +12 -12
- data/lib/llm/{repl → console}/commands/exit.rb +4 -4
- data/lib/llm/{repl → console}/commands/help.rb +1 -1
- data/lib/llm/{repl/commands/compact.rb → console/commands/keep.rb} +11 -9
- data/lib/llm/{repl → console}/commands/model.rb +2 -2
- data/lib/llm/{repl → console}/input/cache.rb +2 -2
- data/lib/llm/{repl → console}/input/char.rb +2 -2
- data/lib/llm/{repl → console}/input/row.rb +1 -1
- data/lib/llm/{repl → console}/input.rb +13 -6
- data/lib/llm/console/markdown/parser.rb +78 -0
- data/lib/llm/{repl → console}/markdown/table.rb +3 -3
- data/lib/llm/{repl → console}/markdown.rb +13 -30
- data/lib/llm/{repl → console}/node.rb +3 -3
- data/lib/llm/{repl → console}/status.rb +11 -11
- data/lib/llm/{repl → console}/stream.rb +9 -9
- data/lib/llm/{repl → console}/walker.rb +1 -1
- data/lib/llm/{repl → console}/window.rb +10 -10
- data/lib/llm/{repl.rb → console.rb} +38 -19
- data/lib/llm/context/deserializer.rb +2 -1
- data/lib/llm/context.rb +1 -0
- data/lib/llm/function/async/reactor.rb +20 -1
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +40 -28
- data/lib/llm/message.rb +7 -0
- data/lib/llm/provider.rb +2 -2
- data/lib/llm/providers/alibaba.rb +1 -1
- data/lib/llm/providers/deepseek.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -0
- data/lib/llm/schema/leaf.rb +34 -2
- data/lib/llm/schema.rb +4 -2
- data/lib/llm/sequel/agent.rb +9 -5
- data/lib/llm/tool/param.rb +5 -1
- data/lib/llm/tool.rb +5 -0
- data/lib/llm/tools/bundle.rb +53 -0
- data/lib/llm/tools/edit-file.rb +7 -2
- data/lib/llm/tools/exec.rb +78 -0
- data/lib/llm/tools/git.rb +27 -26
- data/lib/llm/tools/mkdir.rb +12 -19
- data/lib/llm/tools/read_file.rb +69 -9
- data/lib/llm/tools/rg.rb +20 -24
- data/lib/llm/tools/ruby.rb +17 -25
- data/lib/llm/tools/utils.rb +74 -1
- data/lib/llm/tools/write_file.rb +4 -1
- data/lib/llm/tracer/logger.rb +2 -2
- data/lib/llm/tracer/pretty_logger.rb +4 -4
- data/lib/llm/tracer/telemetry.rb +2 -2
- data/lib/llm/tracer.rb +33 -0
- data/lib/llm/transport/utils.rb +1 -1
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +4 -13
- data/llm.gemspec +7 -8
- metadata +64 -37
- data/lib/llm/tools/shell.rb +0 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c55a3fa1151afad5720fc33bbe60a71feed5577242e53b4e8d4e2f9962a18ed
|
|
4
|
+
data.tar.gz: 3099ef73435ed6d91b003a40e2e5c195a82a8f631418b21e7c8cbce9088a5c76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
>
|
|
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
|
|
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
|
-
###
|
|
420
|
+
### Console
|
|
136
421
|
|
|
137
|
-
* **
|
|
138
|
-
When a request is rate limited or times out, the curses-based
|
|
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
|
-
* **
|
|
143
|
-
The curses-based
|
|
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
|
-
* **
|
|
149
|
-
The curses-based
|
|
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.
|
|
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.
|
|
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="
|
|
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
|
-
>
|
|
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.
|
|
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/
|
|
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.
|
|
248
|
+
<summary>Console (<code>binding.irb</code> for agents)</summary>
|
|
251
249
|
<br>
|
|
252
250
|
|
|
253
|
-
The [LLM::Agent#
|
|
254
|
-
method drops you into a highly capable
|
|
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
|
|
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.
|
|
257
|
+
`binding.irb` but for agents.
|
|
260
258
|
|
|
261
259
|
##### Demo
|
|
262
260
|
|
|
263
|
-
[
|
|
264
|
-
|
|
265
|
-

|
|
261
|
+

|
|
266
262
|
|
|
267
263
|
|
|
268
264
|
##### Installation
|
|
269
265
|
|
|
270
|
-
The
|
|
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
|
-
|
|
280
|
-
across
|
|
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.
|
|
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
|
|
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 $
|
|
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
|
|
427
|
-
agent.
|
|
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 == "
|
|
538
|
+
if function.name == "exec"
|
|
538
539
|
function.return(error: true, type: "policy_error",
|
|
539
|
-
message: "
|
|
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: [
|
|
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
|
-
|
|
633
|
-
requests, tool calls, and other
|
|
634
|
-
|
|
635
|
-
observability backend. All built-in tracers
|
|
636
|
-
so switching between them means changing a
|
|
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
|
|
639
|
-
* [`LLM::Tracer
|
|
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
|
|
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
|
|
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::
|
|
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
|
|
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
|
-
##
|
|
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 [
|
|
981
|
-
|
|
982
|
-
|
|
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
|
|
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
|
|
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/
|
|
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.
|
|
260
|
+
agent.console
|
|
253
261
|
rescue Interrupt
|
|
254
262
|
warn "llm.rb: Bye!"
|
|
255
263
|
rescue => ex
|