llm.rb 13.1.0 → 14.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +320 -0
- data/README.md +340 -31
- data/bin/llm.rb +36 -12
- data/data/anthropic.json +206 -263
- data/data/bedrock.json +2138 -1860
- data/data/deepinfra.json +1003 -624
- data/data/deepseek.json +38 -34
- data/data/google.json +1079 -371
- data/data/mistral.json +448 -368
- data/data/moonshot.json +384 -0
- data/data/openai.json +974 -1343
- data/data/xai.json +154 -126
- data/data/zai.json +191 -191
- data/lib/llm/agent.rb +47 -14
- data/lib/llm/context.rb +71 -88
- data/lib/llm/cost.rb +23 -17
- data/lib/llm/error.rb +0 -8
- data/lib/llm/function/async/task.rb +2 -0
- data/lib/llm/function/fiber/task.rb +2 -0
- data/lib/llm/function/fork/task.rb +2 -0
- data/lib/llm/function/ractor/task.rb +2 -0
- data/lib/llm/function/sequential/group.rb +4 -1
- data/lib/llm/function/sequential/task.rb +1 -1
- data/lib/llm/function/task.rb +4 -0
- data/lib/llm/function/thread/task.rb +2 -0
- data/lib/llm/function.rb +32 -4
- data/lib/llm/guard/loop.rb +89 -0
- data/lib/llm/guard/null.rb +19 -0
- data/lib/llm/guard.rb +61 -0
- data/lib/llm/provider.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
- data/lib/llm/providers/anthropic.rb +1 -8
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
- data/lib/llm/providers/bedrock.rb +1 -8
- data/lib/llm/providers/google/stream_parser.rb +1 -0
- data/lib/llm/providers/google.rb +1 -8
- data/lib/llm/providers/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +1 -8
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +6 -8
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +3 -10
- data/lib/llm/repl/bar.rb +4 -3
- data/lib/llm/repl/buffer.rb +42 -15
- data/lib/llm/repl/color.rb +78 -0
- data/lib/llm/repl/input/char.rb +46 -0
- data/lib/llm/repl/input/row.rb +39 -0
- data/lib/llm/repl/input.rb +251 -66
- data/lib/llm/repl/markdown/table.rb +6 -2
- data/lib/llm/repl/markdown.rb +31 -5
- data/lib/llm/repl/status.rb +38 -3
- data/lib/llm/repl/stream.rb +16 -4
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +25 -5
- data/lib/llm/repl.rb +29 -13
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +29 -0
- data/lib/llm/transformer/null.rb +21 -0
- data/lib/llm/transformer.rb +55 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +12 -2
- data/llm.gemspec +1 -0
- data/resources/deepdive/advanced/cancellation.md +74 -0
- data/resources/deepdive/advanced/compaction.md +83 -0
- data/resources/deepdive/advanced/context.md +267 -0
- data/resources/deepdive/advanced/guard.md +371 -0
- data/resources/deepdive/advanced/tracer.md +180 -0
- data/resources/deepdive/advanced/transformer.md +67 -0
- data/resources/deepdive/advanced/transports.md +45 -0
- data/resources/deepdive/everything_else/audio.md +122 -0
- data/resources/deepdive/everything_else/cost.md +99 -0
- data/resources/deepdive/everything_else/images.md +89 -0
- data/resources/deepdive/everything_else/object.md +108 -0
- data/resources/deepdive/everything_else/ocr.md +48 -0
- data/resources/deepdive/fundamentals/agents.md +202 -0
- data/resources/deepdive/fundamentals/builtin_tools.md +191 -0
- data/resources/deepdive/fundamentals/concurrency.md +104 -0
- data/resources/deepdive/fundamentals/database.md +449 -0
- data/resources/deepdive/fundamentals/embeddings.md +157 -0
- data/resources/deepdive/fundamentals/repl.md +87 -0
- data/resources/deepdive/fundamentals/schema.md +61 -0
- data/resources/deepdive/fundamentals/skills.md +106 -0
- data/resources/deepdive/fundamentals/stream.md +110 -0
- data/resources/deepdive/fundamentals/tools.md +265 -0
- data/resources/deepdive/protocols/a2a.md +106 -0
- data/resources/deepdive/protocols/mcp.md +111 -0
- data/resources/deepdive.md +7 -1
- metadata +36 -3
- data/lib/llm/loop_guard.rb +0 -107
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
|
|
2
|
+
## Built-in tools
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
llm.rb ships with twelve ready-made tools that cover the operations
|
|
9
|
+
a coding or system agent needs most: filesystem work, search, and
|
|
10
|
+
shell commands. Each tool is a subclass of
|
|
11
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
12
|
+
with a name, description, and typed parameters, exactly like a tool
|
|
13
|
+
you would write yourself. Load the whole catalog with
|
|
14
|
+
`require "llm/tools"`.
|
|
15
|
+
|
|
16
|
+
#### How it works
|
|
17
|
+
|
|
18
|
+
When you want to attach every built-in tool to an agent, require
|
|
19
|
+
the catalog and pass the full set of subclasses as the `tools:`
|
|
20
|
+
option. The runtime registers each tool's schema and lets the model
|
|
21
|
+
decide when to call it.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "llm"
|
|
25
|
+
require "llm/tools"
|
|
26
|
+
|
|
27
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
28
|
+
agent = LLM::Agent.new(llm, tools: LLM::Tool.subclasses)
|
|
29
|
+
agent.talk "List the files in this repository"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
#### Why would I use it?
|
|
33
|
+
|
|
34
|
+
The built-in tools cover the operations a coding or system agent
|
|
35
|
+
needs most, so you rarely need to write your own. They also handle
|
|
36
|
+
edge cases correctly: subprocesses get timeouts, interrupts kill
|
|
37
|
+
the child process, and failed calls return structured errors
|
|
38
|
+
instead of crashing the conversation.
|
|
39
|
+
|
|
40
|
+
#### Notes
|
|
41
|
+
|
|
42
|
+
The tools that spawn subprocesses use the optional `test-cmd.rb`
|
|
43
|
+
gem for process management and interrupt handling. The gem is
|
|
44
|
+
required only when those tools load. Every tool returns a Hash with
|
|
45
|
+
an `ok:` key that tells the model whether the call succeeded.
|
|
46
|
+
|
|
47
|
+
### Filesystem
|
|
48
|
+
|
|
49
|
+
#### Overview
|
|
50
|
+
|
|
51
|
+
The filesystem tools let the model read, write, and edit files,
|
|
52
|
+
list and create directories, and move around the working tree.
|
|
53
|
+
The most distinctive is
|
|
54
|
+
[`LLM::Tool::EditFile`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/EditFile.html),
|
|
55
|
+
which replaces an exact snippet and verifies the match count:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
LLM::Tool::EditFile.new.call(
|
|
59
|
+
path: "config.yml",
|
|
60
|
+
before: "port: 3000",
|
|
61
|
+
after: "port: 4000"
|
|
62
|
+
)
|
|
63
|
+
# => {ok: true, replaced: 1}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### How it works
|
|
67
|
+
|
|
68
|
+
Each filesystem tool takes a path and returns a result Hash. The
|
|
69
|
+
`read-file` tool reads a whole file by default and accepts `start:`
|
|
70
|
+
and `stop:` to read a range of lines instead. The `edit-file` tool
|
|
71
|
+
counts occurrences of `before` and raises unless the count matches
|
|
72
|
+
`expected_count`, which defaults to 1.
|
|
73
|
+
|
|
74
|
+
| Tool | Name | Parameters | Purpose |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| [`LLM::Tool::Pwd`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Pwd.html) | `pwd` | none | Report the current working directory |
|
|
77
|
+
| [`LLM::Tool::Ls`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Ls.html) | `ls` | `path`, `glob` | List files and directories, optionally matching a glob |
|
|
78
|
+
| [`LLM::Tool::Chdir`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Chdir.html) | `chdir` | `path` | Change the current working directory |
|
|
79
|
+
| [`LLM::Tool::Mkdir`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Mkdir.html) | `mkdir` | `path` | Create a tree of directories |
|
|
80
|
+
| [`LLM::Tool::ReadFile`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/ReadFile.html) | `read-file` | `path`, `start`, `stop` | Read a file, optionally a range of lines |
|
|
81
|
+
| [`LLM::Tool::WriteFile`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/WriteFile.html) | `write-file` | `path`, `content` | Write a string to a file |
|
|
82
|
+
| [`LLM::Tool::EditFile`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/EditFile.html) | `edit-file` | `path`, `before`, `after`, `expected_count` | Replace an exact snippet in a file |
|
|
83
|
+
|
|
84
|
+
#### Why would I use it?
|
|
85
|
+
|
|
86
|
+
Reading source files to answer questions, writing new files, and
|
|
87
|
+
editing a snippet in place are the bread and butter of a coding
|
|
88
|
+
agent. The filesystem tools give the model all of it without you
|
|
89
|
+
writing a single tool.
|
|
90
|
+
|
|
91
|
+
#### Notes
|
|
92
|
+
|
|
93
|
+
The `chdir` tool changes the working directory for the whole
|
|
94
|
+
process, so subsequent file operations see the new directory. The
|
|
95
|
+
`mkdir` tool creates parent directories, like `mkdir -p`. The `ls`
|
|
96
|
+
tool raises when the path does not exist.
|
|
97
|
+
|
|
98
|
+
### Search
|
|
99
|
+
|
|
100
|
+
#### Overview
|
|
101
|
+
|
|
102
|
+
The search tools find things without reading the whole tree: `rg`
|
|
103
|
+
searches file contents, and `which` locates an executable on the
|
|
104
|
+
PATH. When a method name or a term is needed, the model searches
|
|
105
|
+
for it instead of guessing.
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
LLM::Tool::Rg.new.call(patterns: ["def talk"], path: "lib")
|
|
109
|
+
# => {ok: true, stdout: "lib/llm/context.rb:42:def talk", stderr: ""}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### How it works
|
|
113
|
+
|
|
114
|
+
When you want to search for one or more patterns, call the
|
|
115
|
+
[`LLM::Tool::Rg#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Rg.html#call-instance_method)
|
|
116
|
+
method with an Array of patterns, an optional `path:`, and a
|
|
117
|
+
`timeout:` in seconds. When you want to check whether a command is
|
|
118
|
+
installed, call the
|
|
119
|
+
[`LLM::Tool::Which#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Which.html#call-instance_method)
|
|
120
|
+
method with a `name:`.
|
|
121
|
+
|
|
122
|
+
| Tool | Name | Parameters | Purpose |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| [`LLM::Tool::Rg`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Rg.html) | `rg` | `patterns`, `path`, `timeout` | Recursively search for lines matching patterns |
|
|
125
|
+
| [`LLM::Tool::Which`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Which.html) | `which` | `name` | Locate an executable on the system PATH |
|
|
126
|
+
|
|
127
|
+
#### Why would I use it?
|
|
128
|
+
|
|
129
|
+
Search is how the model finds something without scanning files one
|
|
130
|
+
by one. Asked where a method is defined, the agent searches for it.
|
|
131
|
+
Before running a command, it checks with `which` that the binary
|
|
132
|
+
exists, and falls back to another approach when it does not.
|
|
133
|
+
|
|
134
|
+
#### Notes
|
|
135
|
+
|
|
136
|
+
The `rg` tool runs the ripgrep binary, so `rg` must be installed
|
|
137
|
+
and on the PATH. It refuses to search from the filesystem root and
|
|
138
|
+
rejects the pattern `.` to keep the model from dumping the entire
|
|
139
|
+
tree in one call. The `which` tool is pure Ruby: it scans the PATH
|
|
140
|
+
in order and returns the first directory that contains an
|
|
141
|
+
executable with the given name. When no match is found it returns
|
|
142
|
+
`{ok: false, path: nil}`.
|
|
143
|
+
|
|
144
|
+
### Shell
|
|
145
|
+
|
|
146
|
+
#### Overview
|
|
147
|
+
|
|
148
|
+
The shell tools run real subprocesses: arbitrary commands through
|
|
149
|
+
`shell`, Ruby code through `ruby`, and a fixed set of git actions
|
|
150
|
+
through `git`. All three accept a `timeout:` and kill the child
|
|
151
|
+
process when the model interrupts the turn.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
LLM::Tool::Shell.new.call(
|
|
155
|
+
name: "bundle",
|
|
156
|
+
arguments: ["exec", "rspec", "spec/llm"],
|
|
157
|
+
timeout: 30
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### How it works
|
|
162
|
+
|
|
163
|
+
When you want to run a command and capture its output, call the
|
|
164
|
+
[`LLM::Tool::Shell#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Shell.html#call-instance_method)
|
|
165
|
+
method with a `name:` and optional `arguments:`. The `git` tool
|
|
166
|
+
accepts an `action:` from a fixed set, and the `ruby` tool runs its
|
|
167
|
+
code in a fresh process.
|
|
168
|
+
|
|
169
|
+
| Tool | Name | Parameters | Purpose |
|
|
170
|
+
|---|---|---|---|
|
|
171
|
+
| [`LLM::Tool::Shell`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Shell.html) | `shell` | `name`, `arguments`, `timeout` | Run a shell command |
|
|
172
|
+
| [`LLM::Tool::Git`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Git.html) | `git` | `action`, `arguments`, `timeout` | Run a fixed set of git actions |
|
|
173
|
+
| [`LLM::Tool::Ruby`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Ruby.html) | `ruby` | `code`, `timeout` | Run a string of Ruby code |
|
|
174
|
+
|
|
175
|
+
#### Why would I use it?
|
|
176
|
+
|
|
177
|
+
Running tests, inspecting git history, and executing a snippet of
|
|
178
|
+
Ruby are things a coding agent needs to do. The shell tools make
|
|
179
|
+
those actions first-class, and the timeout keeps a hanging command
|
|
180
|
+
from stalling the conversation.
|
|
181
|
+
|
|
182
|
+
#### Notes
|
|
183
|
+
|
|
184
|
+
[`LLM::Tool::Shell`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool/Shell.html)
|
|
185
|
+
can be dangerous given a low-quality model. Gate it behind
|
|
186
|
+
[`LLM::Agent#confirm`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#confirm)
|
|
187
|
+
or manage the tool loop manually through
|
|
188
|
+
[`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html).
|
|
189
|
+
On interrupt, the running child process is killed. The `ruby` tool
|
|
190
|
+
uses the same Ruby that launched llm.rb. The `git` tool wraps the
|
|
191
|
+
actions `log`, `diff`, `commit`, `checkout`, `branch`, and `show`.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
|
|
2
|
+
## Concurrency
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
llm.rb supports six concurrency strategies for tool execution:
|
|
9
|
+
`:sequential`, `:thread`, `:fiber`, `:async`, `:fork`, and
|
|
10
|
+
`:ractor`. Each implements the same interface
|
|
11
|
+
([`LLM::Function::Task#spawn`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#spawn),
|
|
12
|
+
[`LLM::Function::Task#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#wait),
|
|
13
|
+
[`LLM::Function::Task#alive?`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#alive?),
|
|
14
|
+
[`LLM::Function::Task#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#interrupt!)),
|
|
15
|
+
so the caller never has to care which
|
|
16
|
+
strategy is behind a given task.
|
|
17
|
+
|
|
18
|
+
#### How it works
|
|
19
|
+
|
|
20
|
+
Each strategy creates a task object that wraps a function call.
|
|
21
|
+
[`LLM::Function::Task#spawn`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#spawn)
|
|
22
|
+
starts execution,
|
|
23
|
+
[`LLM::Function::Task#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#wait)
|
|
24
|
+
collects the result,
|
|
25
|
+
[`LLM::Function::Task#alive?`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#alive?)
|
|
26
|
+
checks whether the task is still running, and
|
|
27
|
+
[`LLM::Function::Task#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#interrupt!)
|
|
28
|
+
signals the task to stop.
|
|
29
|
+
|
|
30
|
+
Interruption is reliable across all six.
|
|
31
|
+
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
32
|
+
reaches the tool regardless of whether it runs on a thread, fiber, process,
|
|
33
|
+
or ractor.
|
|
34
|
+
|
|
35
|
+
A tool can rescue
|
|
36
|
+
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
37
|
+
and return a structured error to cancel the tool call, or it can
|
|
38
|
+
re-raise to cancel the turn entirely. When you re-raise
|
|
39
|
+
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
40
|
+
or leave it unrescued the exception is raised on the caller's thread
|
|
41
|
+
and that effectively terminates the turn.
|
|
42
|
+
|
|
43
|
+
The main benefit of rescuing yourself is to let the tool clean up
|
|
44
|
+
and free lingering resources, but you usually want to re-raise so
|
|
45
|
+
the cancel request ends the turn. Otherwise the tool call is
|
|
46
|
+
intercepted but the turn continues.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
##
|
|
50
|
+
# At a high-level
|
|
51
|
+
agent = LLM::Agent.new(llm, concurrency: :fork, tools: [...])
|
|
52
|
+
agent.talk "Run the tools"
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# At a low-level
|
|
56
|
+
fn = FetchStocks.function
|
|
57
|
+
fn.task(:thread).wait
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Why would I use it?
|
|
61
|
+
|
|
62
|
+
Different tools have different execution requirements. IO-bound
|
|
63
|
+
tools benefit from thread or async concurrency. CPU-bound tools
|
|
64
|
+
benefit from ractors or forks. Tools that might crash the process
|
|
65
|
+
should run in a fork for isolation.
|
|
66
|
+
|
|
67
|
+
The common interface means you can change strategies without
|
|
68
|
+
changing your tool code.
|
|
69
|
+
|
|
70
|
+
#### Notes
|
|
71
|
+
|
|
72
|
+
**sequential**: Tools run one at a time on the calling thread. No
|
|
73
|
+
overhead. Best for simple agents or when tool order matters.
|
|
74
|
+
|
|
75
|
+
**thread**: Each tool runs in its own Thread. Releases the GVL
|
|
76
|
+
during blocking IO. Best for IO-bound tools like HTTP calls and
|
|
77
|
+
database queries.
|
|
78
|
+
|
|
79
|
+
**fiber**: Each tool runs in a scheduler-backed Fiber. Requires
|
|
80
|
+
`Fiber.scheduler`. Best for IO-bound tools inside an async
|
|
81
|
+
framework. Much lighter than threads.
|
|
82
|
+
|
|
83
|
+
**async**: Each tool runs as an `Async::Task` inside a managed
|
|
84
|
+
background reactor. Best for IO-bound tools when you want Async's
|
|
85
|
+
structured concurrency model without running your whole application
|
|
86
|
+
inside a reactor. Requires the `async` gem.
|
|
87
|
+
|
|
88
|
+
**fork**: Each tool runs in a forked child process. True parallelism
|
|
89
|
+
and process isolation. Best for shell commands, native extensions,
|
|
90
|
+
or anything you do not want touching the parent's memory. Requires
|
|
91
|
+
the `xchan` gem.
|
|
92
|
+
|
|
93
|
+
**ractor**: Each class-based tool runs in a Ruby Ractor. True
|
|
94
|
+
parallelism without the overhead of forking full processes. Only
|
|
95
|
+
class-based tools are supported. Arguments must be ractor-shareable.
|
|
96
|
+
|
|
97
|
+
| Strategy | Backing | Parallel? | Isolation? | Requires |
|
|
98
|
+
|---|---|---|---|---|
|
|
99
|
+
| `:sequential` | direct call | No | No | None |
|
|
100
|
+
| `:thread` | `Thread` | IO only (GVL) | No | None |
|
|
101
|
+
| `:fiber` | `Fiber.schedule` | Cooperative | No | `Fiber.scheduler` |
|
|
102
|
+
| `:async` | `Async::Reactor` | Cooperative | No | `async` gem |
|
|
103
|
+
| `:fork` | `Kernel.fork` | Yes (process) | Yes (memory) | `xchan` gem |
|
|
104
|
+
| `:ractor` | `Ractor` | Yes (CPU) | Limited | None |
|