llm.rb 16.0.0.pre.9 → 16.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 +27 -1228
- data/README.md +46 -1
- data/docs/deepdive/advanced/cancellation.md +18 -1
- data/docs/deepdive/advanced/compaction.md +4 -1
- data/docs/deepdive/advanced/context.md +2 -1
- data/docs/deepdive/features/concurrency.md +2 -2
- data/docs/deepdive/features/console.md +40 -0
- data/docs/deepdive/features/embeddings.md +7 -2
- data/docs/deepdive/features/files.md +7 -5
- data/docs/deepdive/fundamentals/providers.md +52 -1
- data/docs/deepdive/fundamentals/schema.md +17 -0
- data/docs/deepdive/fundamentals/stream.md +57 -0
- data/docs/deepdive/fundamentals/tools.md +8 -0
- data/lib/llm/tools/utils.rb +25 -1
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +5 -3
- metadata +6 -4
data/README.md
CHANGED
|
@@ -235,9 +235,13 @@ isolation from its parent.
|
|
|
235
235
|
A couple of concurrency strategies require optional, opt-in dependencies.
|
|
236
236
|
The `async` strategy requires the [async](https://github.com/socketry/async)
|
|
237
237
|
gem and the `fork` strategy requires the [xchan.rb](https://github.com/r-uby-dev/xchan.rb)
|
|
238
|
-
gem. The `fiber` strategy requires a scheduler (`Fiber.scheduler`) but by
|
|
238
|
+
gem (`~> 0.24`). The `fiber` strategy requires a scheduler (`Fiber.scheduler`) but by
|
|
239
239
|
default Ruby does not provide one.
|
|
240
240
|
|
|
241
|
+
The `:ractor` strategy is the least interchangeable of the six. It runs
|
|
242
|
+
class-based tools only, and a tool's arguments have to be
|
|
243
|
+
ractor-shareable.
|
|
244
|
+
|
|
241
245
|
```ruby
|
|
242
246
|
require "llm"
|
|
243
247
|
require "llm/tools"
|
|
@@ -349,6 +353,7 @@ llm.rb -n curb # use libcurl as the HTTP transport
|
|
|
349
353
|
llm.rb -x 900 # read timeout of 15 minutes
|
|
350
354
|
llm.rb -t # temporary session, no persistence
|
|
351
355
|
llm.rb -v # print the version
|
|
356
|
+
llm.rb -h # print usage
|
|
352
357
|
```
|
|
353
358
|
</details>
|
|
354
359
|
<details>
|
|
@@ -762,6 +767,41 @@ agent.talk "Hello"
|
|
|
762
767
|
</details>
|
|
763
768
|
|
|
764
769
|
|
|
770
|
+
<details>
|
|
771
|
+
<summary>Usage and cost</summary>
|
|
772
|
+
<br>
|
|
773
|
+
|
|
774
|
+
Every context and agent reports what a conversation has spent and how
|
|
775
|
+
much room is left, and the numbers answer different questions. A
|
|
776
|
+
`token_usage` is the whole conversation, summed as an
|
|
777
|
+
[LLM::Usage](https://r.uby.dev/api-docs/llm.rb/LLM/Usage.html), and it
|
|
778
|
+
is what [LLM::Cost](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html)
|
|
779
|
+
prices against the model registry:
|
|
780
|
+
|
|
781
|
+
```ruby
|
|
782
|
+
require "llm"
|
|
783
|
+
|
|
784
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
785
|
+
agent = LLM::Agent.new(llm)
|
|
786
|
+
agent.talk "Hello"
|
|
787
|
+
|
|
788
|
+
agent.token_usage # => LLM::Usage for the whole conversation
|
|
789
|
+
agent.cost # => LLM::Cost, priced from the registry
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
A `context_used` is one turn's worth - the live size of the most recent
|
|
793
|
+
assistant message - so it is what a context window is really being
|
|
794
|
+
spent on, and `context_usage` is that as a fraction of the window:
|
|
795
|
+
|
|
796
|
+
```ruby
|
|
797
|
+
agent.context_used # => tokens in the latest turn
|
|
798
|
+
agent.context_window # => the model's limit, or nil when unknown
|
|
799
|
+
agent.context_usage # => Rational, eg Rational(100, 10_000)
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
</details>
|
|
803
|
+
|
|
804
|
+
|
|
765
805
|
<details>
|
|
766
806
|
<summary>Observability</summary>
|
|
767
807
|
<br>
|
|
@@ -892,6 +932,11 @@ llm = LLM.moonshot(key: ENV["MOONSHOT_API_KEY"])
|
|
|
892
932
|
llm = LLM.openrouter(key: ENV["OPENROUTER_API_KEY"])
|
|
893
933
|
llm = LLM.alibaba(key: ENV["DASHSCOPE_API_KEY"]) # also: LLM.aliyun
|
|
894
934
|
llm = LLM.mistral(key: ENV["MISTRAL_API_KEY"])
|
|
935
|
+
llm = LLM.bedrock(
|
|
936
|
+
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
|
937
|
+
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
|
|
938
|
+
region: ENV["AWS_REGION"]
|
|
939
|
+
)
|
|
895
940
|
```
|
|
896
941
|
</details>
|
|
897
942
|
|
|
@@ -85,7 +85,8 @@ make is one nobody asked for any more.
|
|
|
85
85
|
|
|
86
86
|
How an interrupt reaches a tool depends on the strategy. The
|
|
87
87
|
`:ractor` strategy delivers it through ractor message passing, and
|
|
88
|
-
`:fork` via a message over the
|
|
88
|
+
`:fork` via a message over the control channel the `xchan.rb` gem
|
|
89
|
+
provides. The `:thread`
|
|
89
90
|
strategy raises it on the thread that runs the tool. The `:fiber`
|
|
90
91
|
and `:async` strategies ask the fiber scheduler for the raise,
|
|
91
92
|
because a scheduled fiber cannot be entered from another thread -
|
|
@@ -109,3 +110,19 @@ The runtime's own path out of a turn does not swallow an interrupt:
|
|
|
109
110
|
[`LLM::Context#try`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#try)
|
|
110
111
|
re-raises anything that is not a retryable failure, and the tracer
|
|
111
112
|
interface has no broad rescues of its own.
|
|
113
|
+
|
|
114
|
+
The names are worth separating.
|
|
115
|
+
[`LLM::Context#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#interrupt!-instance_method)
|
|
116
|
+
and
|
|
117
|
+
[`LLM::Function#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#interrupt!-instance_method)
|
|
118
|
+
are each aliased to `cancel!`, so `cancel!` ends what is in flight and
|
|
119
|
+
tells a tool, and the two spellings are the same call.
|
|
120
|
+
[`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method)
|
|
121
|
+
is something else: it answers a call that has not run with a cancelled
|
|
122
|
+
return and marks it cancelled, which is how a caller declines a pending
|
|
123
|
+
tool rather than interrupting one. A tool hears about an interrupt
|
|
124
|
+
through
|
|
125
|
+
[`LLM::Tool#on_interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#on_interrupt-instance_method),
|
|
126
|
+
or
|
|
127
|
+
[`LLM::Tool#on_cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#on_cancel-instance_method),
|
|
128
|
+
which takes precedence when both are defined.
|
|
@@ -24,7 +24,10 @@ It preserves tool call/return pairs so the conversation never
|
|
|
24
24
|
contains an orphaned result.
|
|
25
25
|
|
|
26
26
|
The `keep:` parameter accepts an integer count or a percentage
|
|
27
|
-
string like `"80%"`.
|
|
27
|
+
string like `"80%"`, and defaults to `64`. When `Truncate` is used
|
|
28
|
+
through a context or an agent, `keep:` is given as
|
|
29
|
+
`compactor_options: {keep: N}` rather than to the compactor itself.
|
|
30
|
+
The default compactor is
|
|
28
31
|
[`LLM::Compactor::Null`](https://r.uby.dev/api-docs/llm.rb/LLM/Compactor/Null.html),
|
|
29
32
|
which does nothing. A compactor can also be used standalone:
|
|
30
33
|
|
|
@@ -332,7 +332,8 @@ which can be helpful when the interrupt is temporary.
|
|
|
332
332
|
|
|
333
333
|
The mechanism is the same across all six concurrency strategies.
|
|
334
334
|
The `:ractor` strategy delivers the interrupt through ractor
|
|
335
|
-
message passing. The `:fork` strategy delivers it
|
|
335
|
+
message passing. The `:fork` strategy delivers it over the control
|
|
336
|
+
channel the `xchan.rb` gem provides.
|
|
336
337
|
|
|
337
338
|
### Messages
|
|
338
339
|
|
|
@@ -94,7 +94,7 @@ inside a reactor. Requires the `async` gem.
|
|
|
94
94
|
**fork**: Each tool runs in a forked child process. True parallelism
|
|
95
95
|
and process isolation. Best for shell commands, native extensions,
|
|
96
96
|
or anything you do not want touching the parent's memory. Requires
|
|
97
|
-
the `xchan` gem.
|
|
97
|
+
the `xchan.rb` gem (`~> 0.24`).
|
|
98
98
|
|
|
99
99
|
**ractor**: Each class-based tool runs in a Ruby Ractor. True
|
|
100
100
|
parallelism without the overhead of forking full processes. Only
|
|
@@ -106,5 +106,5 @@ class-based tools are supported. Arguments must be ractor-shareable.
|
|
|
106
106
|
| `:thread` | `Thread` | IO only (GVL) | No | None |
|
|
107
107
|
| `:fiber` | `Fiber.schedule` | Cooperative | No | `Fiber.scheduler` |
|
|
108
108
|
| `:async` | `Async::Reactor` | Cooperative | No | `async` gem |
|
|
109
|
-
| `:fork` | `Kernel.fork` | Yes (process) | Yes (memory) | `xchan` gem |
|
|
109
|
+
| `:fork` | `Kernel.fork` | Yes (process) | Yes (memory) | `xchan.rb` gem |
|
|
110
110
|
| `:ractor` | `Ractor` | Yes (CPU) | Limited | None |
|
|
@@ -79,6 +79,46 @@ and
|
|
|
79
79
|
helpers write a formatted `user:` message with a trailing newline,
|
|
80
80
|
and `LLM::Command#write_message` matches the same interface.
|
|
81
81
|
|
|
82
|
+
### The `llm.rb` executable
|
|
83
|
+
|
|
84
|
+
#### Overview
|
|
85
|
+
|
|
86
|
+
The console also starts from a shell. After the gem is installed,
|
|
87
|
+
`llm.rb` puts a console on your PATH and opens a session in the current
|
|
88
|
+
directory, so there is no Ruby file to write.
|
|
89
|
+
|
|
90
|
+
#### How it works
|
|
91
|
+
|
|
92
|
+
Run `llm.rb` with no arguments to auto-detect a provider from the usual
|
|
93
|
+
environment variables, or name one with `-p`. The session for the
|
|
94
|
+
current directory is kept under `~/.llm.rb/` and restored on the next
|
|
95
|
+
visit, unless `-t` asks for a temporary one:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
llm.rb # auto-detect from $PROVIDER_API_KEY
|
|
99
|
+
llm.rb -p openai # use OpenAI explicitly
|
|
100
|
+
llm.rb -m gpt-5.6 # use a model other than the provider default
|
|
101
|
+
llm.rb -c thread # run tool calls on a separate thread
|
|
102
|
+
llm.rb -n curb # use libcurl as the HTTP transport
|
|
103
|
+
llm.rb -x 900 # read timeout of 15 minutes
|
|
104
|
+
llm.rb -t # temporary session, no persistence
|
|
105
|
+
llm.rb -v # print the version
|
|
106
|
+
llm.rb -h # print usage
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Why would I use it?
|
|
110
|
+
|
|
111
|
+
It is the shortest path to a working session: no script, no `require`,
|
|
112
|
+
and a conversation that is still there tomorrow. Reach for
|
|
113
|
+
`agent.console` instead when the agent needs a provider, tools, or a
|
|
114
|
+
tracer that only your application can supply.
|
|
115
|
+
|
|
116
|
+
#### Notes
|
|
117
|
+
|
|
118
|
+
The switches are the console's own, so `-c`, `-m`, and `-p` set the
|
|
119
|
+
same things `agent.console` takes from its agent. `-v` and `-h` print
|
|
120
|
+
and exit.
|
|
121
|
+
|
|
82
122
|
### Switch the model
|
|
83
123
|
|
|
84
124
|
The active model is exposed through
|
|
@@ -42,8 +42,8 @@ pattern is one of the most common AI workflows.
|
|
|
42
42
|
|
|
43
43
|
#### Notes
|
|
44
44
|
|
|
45
|
-
Not all providers support embeddings. The following providers have
|
|
46
|
-
|
|
45
|
+
Not all providers support embeddings. The following providers have an
|
|
46
|
+
`embed` method of their own:
|
|
47
47
|
|
|
48
48
|
- OpenAI (`text-embedding-3-small`)
|
|
49
49
|
- DeepInfra (`BAAI/bge-m3`)
|
|
@@ -53,6 +53,11 @@ an `embed` method:
|
|
|
53
53
|
- OpenRouter (`openai/text-embedding-3-small`)
|
|
54
54
|
- Bedrock (the model is given per call)
|
|
55
55
|
|
|
56
|
+
The providers built on OpenAI inherit its `embed`, so DeepSeek, xAI,
|
|
57
|
+
Z.ai, Moonshot, Alibaba, and llama.cpp answer it too, against their own
|
|
58
|
+
OpenAI-compatible endpoint. Anthropic is the exception: it defines no
|
|
59
|
+
`embed`, so the call raises `NotImplementedError`.
|
|
60
|
+
|
|
56
61
|
### Vector stores (OpenAI)
|
|
57
62
|
|
|
58
63
|
#### Overview
|
|
@@ -45,8 +45,10 @@ assistants that read them, get their content.
|
|
|
45
45
|
|
|
46
46
|
#### Notes
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
Anthropic, Google, and OpenAI implement the Files API, and so do the
|
|
49
|
+
providers built on OpenAI's - Moonshot and Alibaba among them, which
|
|
50
|
+
inherit it. A provider that has no Files API of its own, such as
|
|
51
|
+
DeepSeek, Mistral, or xAI, raises `NotImplementedError`. `download` is
|
|
52
|
+
OpenAI-only, so the OpenAI-compatible providers inherit that too. A file
|
|
53
|
+
id belongs to the provider that minted it: DeepSeek, for example, raises
|
|
54
|
+
`LLM::PromptError` when it is handed a file id from another provider.
|
|
@@ -318,4 +318,55 @@ endpoint. Token Plan users should point the provider at their own
|
|
|
318
318
|
Token Plan URL via `DASHSCOPE_API_HOST` or `host:`. Check your
|
|
319
319
|
Model Studio dashboard for the correct endpoint.
|
|
320
320
|
|
|
321
|
-
Model metadata ships in `data/alibaba.json` for the registry.
|
|
321
|
+
Model metadata ships in `data/alibaba.json` for the registry.
|
|
322
|
+
|
|
323
|
+
### Bedrock
|
|
324
|
+
|
|
325
|
+
#### Overview
|
|
326
|
+
|
|
327
|
+
[`LLM::Bedrock`](https://r.uby.dev/api-docs/llm.rb/LLM/Bedrock.html)
|
|
328
|
+
talks to [Amazon Bedrock](https://aws.amazon.com/bedrock/) through the
|
|
329
|
+
[Converse API](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html),
|
|
330
|
+
which reaches models from Anthropic, Meta, Mistral, Cohere, and others
|
|
331
|
+
behind one endpoint. It is created with
|
|
332
|
+
[`LLM.bedrock`](https://r.uby.dev/api-docs/llm.rb/LLM.html#bedrock-class_method).
|
|
333
|
+
|
|
334
|
+
#### How it works
|
|
335
|
+
|
|
336
|
+
Bedrock is the one provider that does not take a `key:`. It signs each
|
|
337
|
+
request with AWS Signature V4, so it wants an access key, a secret key,
|
|
338
|
+
and a region:
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
require "llm"
|
|
342
|
+
|
|
343
|
+
llm = LLM.bedrock(
|
|
344
|
+
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
|
345
|
+
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
|
|
346
|
+
region: "us-east-1"
|
|
347
|
+
)
|
|
348
|
+
ctx = LLM::Context.new(llm)
|
|
349
|
+
ctx.talk "Hello"
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Each of the three falls back to its own environment variable
|
|
353
|
+
(`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`), so
|
|
354
|
+
`LLM.bedrock` alone works in an environment that already has
|
|
355
|
+
credentials. `region:` defaults to `us-east-1`, and the host is
|
|
356
|
+
`bedrock-runtime.<region>.amazonaws.com`.
|
|
357
|
+
|
|
358
|
+
#### Why would I use it?
|
|
359
|
+
|
|
360
|
+
One credential reaches every model Bedrock serves, so a model can be
|
|
361
|
+
swapped with `model:` alone rather than by moving to another vendor's
|
|
362
|
+
API. For an application that already runs on AWS, it also keeps the
|
|
363
|
+
traffic inside the account's own region.
|
|
364
|
+
|
|
365
|
+
#### Notes
|
|
366
|
+
|
|
367
|
+
A model is named by its regional id rather than a short name - the
|
|
368
|
+
entries in `data/bedrock.json` look like
|
|
369
|
+
`anthropic.claude-sonnet-4-20250514-v1:0` or `google.gemma-4-31b`, and
|
|
370
|
+
some are prefixed by the region that serves them (`us-gov.`, `eu.`,
|
|
371
|
+
`global.`). Passing `key:` raises `ArgumentError`, because there is no
|
|
372
|
+
API key to pass.
|
|
@@ -133,6 +133,23 @@ renders the schema as a prompt-friendly string. `required` and
|
|
|
133
133
|
`defaults` refer to properties that already exist, so declare the
|
|
134
134
|
property first and mark it afterwards.
|
|
135
135
|
|
|
136
|
+
A property also takes the constraints its type supports, as options.
|
|
137
|
+
`min:` and `max:` bound a number's value or a string's length,
|
|
138
|
+
`multiple_of:` constrains a number, `const:` fixes one value, `enum:`
|
|
139
|
+
lists the allowed values, and `default:` gives one. Pass
|
|
140
|
+
`required: true`, or `required: false`, to mark a single property
|
|
141
|
+
without the class-level call:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class Form < LLM::Schema
|
|
145
|
+
property :name, String, "The name", min: 2, max: 8
|
|
146
|
+
property :age, Integer, "The age", min: 0, max: 120
|
|
147
|
+
property :currency, String, "The currency", enum: %w[usd eur]
|
|
148
|
+
property :note, String, "An optional note", required: false
|
|
149
|
+
required %i[name age currency]
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
136
153
|
### Parsing
|
|
137
154
|
|
|
138
155
|
#### Overview
|
|
@@ -151,3 +151,60 @@ agent.talk "Explain Ruby fibers."
|
|
|
151
151
|
A stream subclass gives you visibility into more than just content chunks. React to tool
|
|
152
152
|
calls as they happen, show compaction progress, or integrate with
|
|
153
153
|
an existing observability stack.
|
|
154
|
+
|
|
155
|
+
### Streamed tool execution
|
|
156
|
+
|
|
157
|
+
#### Overview
|
|
158
|
+
|
|
159
|
+
A tool call is a round trip. By default the runtime makes it after the
|
|
160
|
+
response has finished streaming, so the model sits idle for the length
|
|
161
|
+
of the tool. A stream can start the tool the moment its call is parsed
|
|
162
|
+
instead, and hand the result to the turn when it is asked for.
|
|
163
|
+
|
|
164
|
+
#### How it works
|
|
165
|
+
|
|
166
|
+
Start the tool from
|
|
167
|
+
[`LLM::Stream#on_tool_call`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_tool_call)
|
|
168
|
+
and enqueue its task on
|
|
169
|
+
[`LLM::Stream#queue`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#queue-instance_method).
|
|
170
|
+
[`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait-instance_method)
|
|
171
|
+
drains that queue in preference to the pending functions, so a tool that
|
|
172
|
+
was started early is waited for, not run twice:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
class FastStream < LLM::Stream
|
|
176
|
+
def on_tool_call(tool)
|
|
177
|
+
queue << tool.task(:thread).tap(&:spawn)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
182
|
+
agent = LLM::Agent.new(llm, stream: FastStream.new)
|
|
183
|
+
agent.talk "Read every file in lib/ and summarise them."
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
[`LLM::Stream::Queue`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream/Queue.html)
|
|
187
|
+
takes either a running task or an immediate
|
|
188
|
+
[`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html),
|
|
189
|
+
and
|
|
190
|
+
[`LLM::Stream#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#wait-instance_method)
|
|
191
|
+
resolves whatever it holds and returns the returns. The task type is
|
|
192
|
+
remembered, so no strategy is passed at wait time.
|
|
193
|
+
|
|
194
|
+
#### Why would I use it?
|
|
195
|
+
|
|
196
|
+
A turn that calls several tools spends most of its wall-clock time
|
|
197
|
+
waiting. Starting each tool as its call arrives overlaps that wait with
|
|
198
|
+
the rest of the stream, so a turn that reads ten files costs roughly one
|
|
199
|
+
file's latency rather than ten.
|
|
200
|
+
|
|
201
|
+
#### Notes
|
|
202
|
+
|
|
203
|
+
The queue is only consulted when it is non-empty. A turn that enqueues
|
|
204
|
+
nothing behaves exactly as before, and a turn that enqueues from
|
|
205
|
+
`on_tool_call` still runs any *other* pending function through the
|
|
206
|
+
strategy given to
|
|
207
|
+
[`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait-instance_method).
|
|
208
|
+
A cancel reaches the queue
|
|
209
|
+
([`LLM::Stream::Queue#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream/Queue.html#interrupt!-instance_method)),
|
|
210
|
+
so tools started this way are interrupted with the rest of the turn.
|
|
@@ -108,6 +108,14 @@ in the How it works section) or with
|
|
|
108
108
|
[`LLM::Tool.set`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#set-class_method)
|
|
109
109
|
(see the Set subsection). Both approaches work the same way.
|
|
110
110
|
|
|
111
|
+
A parameter takes the same constraints a schema property does, as
|
|
112
|
+
options: `min:` and `max:` bound a number or a string's length,
|
|
113
|
+
`multiple_of:` constrains a number, `const:` fixes one value, and
|
|
114
|
+
`enum:` lists the allowed values. So
|
|
115
|
+
`parameter :age, Integer, "Age", min: 0, max: 120` sends the range to
|
|
116
|
+
the model rather than a bare integer. The Schema topic covers the
|
|
117
|
+
keywords in full.
|
|
118
|
+
|
|
111
119
|
### Confirmation
|
|
112
120
|
|
|
113
121
|
#### Overview
|
data/lib/llm/tools/utils.rb
CHANGED
|
@@ -48,12 +48,22 @@ class LLM::Tool
|
|
|
48
48
|
# Wait for a command to finish, or abort
|
|
49
49
|
# with an error when it exceeds the
|
|
50
50
|
# specified timeout.
|
|
51
|
+
#
|
|
52
|
+
# **A command is waited on until its status is decided, not only until it
|
|
53
|
+
# has stopped running.** `running?` answers whether the process is there,
|
|
54
|
+
# and `success?` answers `nil` until the command has been reaped - so a
|
|
55
|
+
# loop that trusts `running?` alone can leave before the status exists, and
|
|
56
|
+
# the caller reads `ok: nil` beside an output the read had waited for.
|
|
57
|
+
#
|
|
58
|
+
# **`running?` is what decides it.** The command reaps the process as it
|
|
59
|
+
# answers, so every turn of this loop is what makes a status exist - which
|
|
60
|
+
# is why the condition is asked again on each turn rather than read once.
|
|
51
61
|
# @param [Test::Command] command
|
|
52
62
|
# @param [Integer] timeout
|
|
53
63
|
# @return [void]
|
|
54
64
|
def wait(command:, timeout:)
|
|
55
65
|
start = now
|
|
56
|
-
while command.running?
|
|
66
|
+
while command.running? || status_unavailable?(command)
|
|
57
67
|
if now - start > timeout
|
|
58
68
|
command.kill!
|
|
59
69
|
raise "command timed out after #{timeout}s"
|
|
@@ -96,6 +106,20 @@ class LLM::Tool
|
|
|
96
106
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
97
107
|
end
|
|
98
108
|
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
##
|
|
112
|
+
# Whether a command has not been reaped yet.
|
|
113
|
+
#
|
|
114
|
+
# `success?` answers `nil` until the status exists, and a command that was
|
|
115
|
+
# never found is the one case where it never will: there was nothing to
|
|
116
|
+
# reap, and `not_found?` is the answer the caller wants.
|
|
117
|
+
# @param [Test::Command] command
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def status_unavailable?(command)
|
|
120
|
+
command.success?.nil? and !command.not_found?
|
|
121
|
+
end
|
|
122
|
+
|
|
99
123
|
##
|
|
100
124
|
# requires test-cmd.rb
|
|
101
125
|
LLM.require "test-cmd.rb", "~> 2.7.1"
|
data/lib/llm/version.rb
CHANGED
data/llm.gemspec
CHANGED
|
@@ -10,9 +10,11 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
|
|
11
11
|
spec.summary = "An agentic runtime for Ruby"
|
|
12
12
|
spec.description = <<~DESCRIPTION
|
|
13
|
-
llm.rb is
|
|
14
|
-
It has zero runtime dependencies by default,
|
|
15
|
-
|
|
13
|
+
llm.rb is an agentic runtime for CRuby.
|
|
14
|
+
It has zero runtime dependencies by default,
|
|
15
|
+
supports concurrent and parallel tool execution
|
|
16
|
+
and has a single coherent API that spans
|
|
17
|
+
14+ providers.
|
|
16
18
|
DESCRIPTION
|
|
17
19
|
|
|
18
20
|
spec.license = "MIT"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 16.0.0
|
|
4
|
+
version: 16.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robert Gleeson
|
|
@@ -362,9 +362,11 @@ dependencies:
|
|
|
362
362
|
- !ruby/object:Gem::Version
|
|
363
363
|
version: '2.45'
|
|
364
364
|
description: |
|
|
365
|
-
llm.rb is
|
|
366
|
-
It has zero runtime dependencies by default,
|
|
367
|
-
|
|
365
|
+
llm.rb is an agentic runtime for CRuby.
|
|
366
|
+
It has zero runtime dependencies by default,
|
|
367
|
+
supports concurrent and parallel tool execution
|
|
368
|
+
and has a single coherent API that spans
|
|
369
|
+
14+ providers.
|
|
368
370
|
email:
|
|
369
371
|
- robert@r.uby.dev
|
|
370
372
|
executables:
|