llm.rb 11.0.0 → 11.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 +126 -1
- data/README.md +58 -18
- data/lib/llm/a2a/transport/http.rb +9 -8
- data/lib/llm/a2a.rb +14 -7
- data/lib/llm/agent.rb +6 -3
- data/lib/llm/context.rb +41 -6
- data/lib/llm/function/array.rb +6 -0
- data/lib/llm/function.rb +38 -4
- data/lib/llm/json_adapter.rb +8 -2
- data/lib/llm/mcp/transport/http.rb +7 -5
- data/lib/llm/mcp.rb +6 -7
- data/lib/llm/object/builder.rb +1 -0
- data/lib/llm/object.rb +9 -0
- data/lib/llm/provider.rb +1 -18
- data/lib/llm/providers/anthropic/files.rb +6 -6
- data/lib/llm/providers/anthropic/models.rb +1 -1
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/models.rb +4 -4
- data/lib/llm/providers/bedrock/signature.rb +3 -3
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/google/files.rb +5 -5
- data/lib/llm/providers/google/images.rb +1 -1
- data/lib/llm/providers/google/models.rb +1 -1
- data/lib/llm/providers/google.rb +2 -2
- data/lib/llm/providers/ollama/models.rb +1 -1
- data/lib/llm/providers/ollama.rb +2 -2
- data/lib/llm/providers/openai/audio.rb +3 -3
- data/lib/llm/providers/openai/files.rb +5 -5
- data/lib/llm/providers/openai/images.rb +3 -3
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/moderations.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +3 -3
- data/lib/llm/providers/openai/vector_stores.rb +11 -11
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/schema.rb +23 -5
- data/lib/llm/skill.rb +44 -14
- data/lib/llm/tool.rb +21 -0
- data/lib/llm/tracer/telemetry.rb +3 -1
- data/lib/llm/transport/curb.rb +246 -0
- data/lib/llm/transport/execution.rb +1 -1
- data/lib/llm/transport/http.rb +9 -4
- data/lib/llm/transport/net_http_adapter.rb +61 -0
- data/lib/llm/transport/persistent_http.rb +10 -5
- data/lib/llm/transport/request.rb +121 -0
- data/lib/llm/transport/response/curb.rb +112 -0
- data/lib/llm/transport/response.rb +1 -0
- data/lib/llm/transport/utils.rb +42 -17
- data/lib/llm/transport.rb +17 -45
- data/lib/llm/version.rb +1 -1
- data/llm.gemspec +3 -3
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb1ffd1e0ecb17422014ec8f75c8b729f74d0a7cef4fd4e12681ef254411b24a
|
|
4
|
+
data.tar.gz: e5a7815d52c6fa99a38dec111c6d71aef9782272b97bfbad81e8f5bee913f918
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7c2d1dac8ef97a5be2540896828b523ce1491e43f2fd8c78e53b8fbab34432bf6dedaee066afd43d261fb8f4e2fb9f7c3d8c112de83e60ee809d6ef77f41feb
|
|
7
|
+
data.tar.gz: d5073e8c5c156739cff4c68c65c4efaf88403229841ada3cdd1adb4920dfb58c00017ae7dbd73d12ef17b6c191b0b71ab4f15fd0c0ae29991dcffb1b840381d0
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,131 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## v11.2.0
|
|
6
|
+
|
|
7
|
+
Changes since `v11.1.0`.
|
|
8
|
+
|
|
9
|
+
This release adds `LLM::Function#skill?` and `LLM::Tool#skill?` so
|
|
10
|
+
callers can inspect whether a function or tool is backed by a skill.
|
|
11
|
+
|
|
12
|
+
It introduces `LLM::Transport::Request` as a transport-agnostic request
|
|
13
|
+
object so providers no longer depend directly on `Net::HTTP` request
|
|
14
|
+
classes, and adds an optional Curb (libcurl) backend alongside symbolic
|
|
15
|
+
transport shortcuts such as `transport: :curb`.
|
|
16
|
+
|
|
17
|
+
MCP and A2A clients now accept `persistent: true` matching provider configuration.
|
|
18
|
+
Several fixes land for tool return callback emission, function comparison by
|
|
19
|
+
tool call ID, function array filtering, skill tool inheritance, and JSON generator
|
|
20
|
+
state compatibility on Ruby 4.
|
|
21
|
+
|
|
22
|
+
### Add
|
|
23
|
+
|
|
24
|
+
* **Add `LLM::Function#skill?`** <br>
|
|
25
|
+
Add `skill?` to `LLM::Function` so callers can check whether a
|
|
26
|
+
function is backed by a skill tool.
|
|
27
|
+
|
|
28
|
+
* **Add `LLM::Tool.skill?` and `LLM::Tool#skill?`** <br>
|
|
29
|
+
Add class-level `skill?` and instance-level `skill?` to
|
|
30
|
+
`LLM::Tool`, matching the existing `mcp?` and `a2a?` pattern.
|
|
31
|
+
|
|
32
|
+
* **Add `LLM::Transport::Request`** <br>
|
|
33
|
+
Add `LLM::Transport::Request` as a transport-agnostic request object
|
|
34
|
+
and update providers to build requests without depending directly on
|
|
35
|
+
Net::HTTP request classes. The built-in Net::HTTP transports still
|
|
36
|
+
accept existing Net::HTTP request objects through a compatibility
|
|
37
|
+
bridge, while alternative transports can handle the generic request
|
|
38
|
+
shape directly.
|
|
39
|
+
|
|
40
|
+
* **Add optional Curb transport support** <br>
|
|
41
|
+
Add `LLM::Transport::Curb`, an optional libcurl-backed transport
|
|
42
|
+
that can be selected with `transport: :curb`. Providers already
|
|
43
|
+
emit `LLM::Transport::Request` objects, so the Curb backend can
|
|
44
|
+
execute requests without routing through Net::HTTP.
|
|
45
|
+
|
|
46
|
+
* **Add symbolic transport shortcuts** <br>
|
|
47
|
+
Allow providers, MCP HTTP clients, and A2A HTTP clients to accept
|
|
48
|
+
transport shortcuts such as `transport: :curb` and
|
|
49
|
+
`transport: :net_http_persistent`.
|
|
50
|
+
|
|
51
|
+
* **Add persistent HTTP selection to MCP and A2A clients** <br>
|
|
52
|
+
Allow MCP and A2A HTTP clients to accept `persistent: true`, matching
|
|
53
|
+
provider configuration and selecting the persistent Net::HTTP
|
|
54
|
+
transport by default.
|
|
55
|
+
|
|
56
|
+
### Fix
|
|
57
|
+
|
|
58
|
+
* **Support JSON generation state on Ruby 4** <br>
|
|
59
|
+
Handle JSON generator state objects in the standard JSON adapter so
|
|
60
|
+
schema objects serialize correctly when Ruby 4 calls custom `to_json`
|
|
61
|
+
methods during provider request generation.
|
|
62
|
+
|
|
63
|
+
* **Emit tool return callbacks for direct context waits** <br>
|
|
64
|
+
Emit `LLM::Stream#on_tool_return` when `LLM::Context#wait` executes
|
|
65
|
+
pending tool work directly instead of draining `LLM::Stream::Queue`.
|
|
66
|
+
|
|
67
|
+
* **Emit confirmed tool return callbacks once** <br>
|
|
68
|
+
Emit `LLM::Stream#on_tool_return` for confirmed and cancelled tool
|
|
69
|
+
calls, and exclude confirmed functions from later waits so mixed
|
|
70
|
+
confirmed and unconfirmed tool batches do not execute confirmed tools
|
|
71
|
+
twice.
|
|
72
|
+
|
|
73
|
+
* **Compare functions by tool call ID** <br>
|
|
74
|
+
Add `LLM::Function#==`, `#eql?`, and `#hash` so pending function
|
|
75
|
+
collections can compare tool calls by provider-assigned ID instead of
|
|
76
|
+
object identity.
|
|
77
|
+
|
|
78
|
+
* **Preserve function array behavior after filtering** <br>
|
|
79
|
+
Preserve `LLM::Function::Array` behavior when subtracting function
|
|
80
|
+
arrays so filtered tool batches can still spawn through the normal
|
|
81
|
+
function array API.
|
|
82
|
+
|
|
83
|
+
* **Prevent skills from inheriting skill-backed tools** <br>
|
|
84
|
+
Exclude skill-backed tools when a skill sub-agent uses `tools:
|
|
85
|
+
inherit`, preventing skills loaded through a parent context from
|
|
86
|
+
being recursively exposed to nested skill agents.
|
|
87
|
+
|
|
88
|
+
## v11.1.0
|
|
89
|
+
|
|
90
|
+
Changes since `v11.0.0`.
|
|
91
|
+
|
|
92
|
+
This release adds the `inherit` directive for skill sub-agents so they can
|
|
93
|
+
inherit access to the local, MCP, and A2A tools available to their parent
|
|
94
|
+
agent. It introduces class-level `required %i[...]` declarations to
|
|
95
|
+
`LLM::Schema` and wraps `LLM::Function#arguments` in `LLM::Object` for
|
|
96
|
+
method-style argument access. The OpenTelemetry tracer now samples all spans
|
|
97
|
+
regardless of environment, and the tool-call loop repair step prevents stale
|
|
98
|
+
history from being sent on follow-up requests.
|
|
99
|
+
|
|
100
|
+
### Add
|
|
101
|
+
|
|
102
|
+
* **Add support for the `inherit` directive in skills** <br>
|
|
103
|
+
Add support for the `inherit` directive so a skill sub-agent can
|
|
104
|
+
inherit access to the local, MCP, and A2A tools available to its
|
|
105
|
+
parent agent.
|
|
106
|
+
|
|
107
|
+
* **Add class-level `required %i[...]` support to `LLM::Schema`** <br>
|
|
108
|
+
Add class-level `required %i[...]` declarations to `LLM::Schema`, so
|
|
109
|
+
schema classes can mark existing properties as required the same way
|
|
110
|
+
`LLM::Tool` params already can.
|
|
111
|
+
|
|
112
|
+
* **Wrap function arguments in `LLM::Object`** <br>
|
|
113
|
+
Wrap `LLM::Function#arguments` in `LLM::Object`, so function
|
|
114
|
+
implementations can read arguments with method-style access while
|
|
115
|
+
still invoking runners with keyword arguments.
|
|
116
|
+
|
|
117
|
+
### Fix
|
|
118
|
+
|
|
119
|
+
* **Ensure all traces are sampled regardless of environment** <br>
|
|
120
|
+
Explicitly pass `Samplers::ALWAYS_ON` when creating the OpenTelemetry
|
|
121
|
+
`TracerProvider` so the in-memory exporter always captures every span,
|
|
122
|
+
regardless of the `OTEL_TRACES_SAMPLER` environment variable.
|
|
123
|
+
|
|
124
|
+
* **Always close the tool call loop before sending follow-up requests** <br>
|
|
125
|
+
Add a repair step in `Context#talk` that closes assistant tool-call
|
|
126
|
+
messages without matching tool responses before the next provider
|
|
127
|
+
request is sent. This prevents stale tool-call history from being sent
|
|
128
|
+
on follow-up requests, which some providers reject as invalid.
|
|
129
|
+
|
|
5
130
|
## v11.0.0
|
|
6
131
|
|
|
7
132
|
Changes since `v10.0.0`.
|
|
@@ -91,7 +216,7 @@ requests outside `#session`, `LLM::Function#def` as a short alias for
|
|
|
91
216
|
|
|
92
217
|
* **Fix context and agent JSON serialization through `LLM.json`** <br>
|
|
93
218
|
Fix `LLM::Context#to_json` and `LLM::Agent#to_json` to serialize
|
|
94
|
-
through `LLM.json.dump(...)` instead of plain `to_json`.
|
|
219
|
+
through `LLM.json.dump(...)` instead of plain `to_json`.
|
|
95
220
|
|
|
96
221
|
* **Fix block-form ORM agent DSL forwarding** <br>
|
|
97
222
|
Fix block-form `model { ... }`, `tools { ... }`, and
|
data/README.md
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
</a>
|
|
5
5
|
</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
<a href="https://
|
|
8
|
-
<img src="https://img.shields.io/badge/docs-
|
|
7
|
+
<a href="https://llmrb.github.io/llm.rb">
|
|
8
|
+
<img src="https://img.shields.io/badge/docs-llmrb.github.io-blue.svg" alt="Official llm.rb website">
|
|
9
9
|
</a>
|
|
10
10
|
<a href="https://opensource.org/license/0bsd">
|
|
11
11
|
<img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License">
|
|
12
12
|
</a>
|
|
13
13
|
<a href="https://github.com/llmrb/llm.rb/tags">
|
|
14
|
-
<img src="https://img.shields.io/badge/version-11.
|
|
14
|
+
<img src="https://img.shields.io/badge/version-11.2.0-green.svg?" alt="Version">
|
|
15
15
|
</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
@@ -22,8 +22,7 @@ llm.rb is Ruby's most capable AI runtime.
|
|
|
22
22
|
It runs on Ruby's standard library by default. loads optional pieces
|
|
23
23
|
only when needed, and offers a single runtime for providers, agents,
|
|
24
24
|
tools, skills, MCP, A2A (Agent2Agent), RAG (vector stores & embeddings),
|
|
25
|
-
streaming, files, and persisted state.
|
|
26
|
-
[available for mruby](https://github.com/llmrb/mruby-llm).
|
|
25
|
+
streaming, files, and persisted state.
|
|
27
26
|
|
|
28
27
|
It supports OpenAI, OpenAI-compatible endpoints, Anthropic, Google
|
|
29
28
|
Gemini, DeepSeek, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp. It
|
|
@@ -31,6 +30,11 @@ also includes built-in ActiveRecord and Sequel support, plus concurrent
|
|
|
31
30
|
tool execution through threads, tasks (via async gem), fibers, ractors,
|
|
32
31
|
and fork (via xchan.rb gem).
|
|
33
32
|
|
|
33
|
+
As a bonus, llm.rb is also available to embedded systems [via mruby](https://github.com/llmrb/mruby-llm#readme),
|
|
34
|
+
to the browser and edge devices [via WebAssembly](https://github.com/llmrb/wasm-llm#readme),
|
|
35
|
+
and has first-class [Rails support](https://github.com/llmrb/rails-llm#readme)
|
|
36
|
+
via a separate gem.
|
|
37
|
+
|
|
34
38
|
## Quick start
|
|
35
39
|
|
|
36
40
|
#### LLM::Context
|
|
@@ -90,7 +94,7 @@ class Agent < LLM::Agent
|
|
|
90
94
|
confirm "delete-file"
|
|
91
95
|
|
|
92
96
|
def on_tool_confirmation(fn, strategy)
|
|
93
|
-
path = fn.arguments
|
|
97
|
+
path = fn.arguments.path
|
|
94
98
|
if path.start_with?("/tmp/")
|
|
95
99
|
fn.spawn(strategy).wait
|
|
96
100
|
else
|
|
@@ -171,7 +175,9 @@ ctx.talk(ctx.wait(:call)) while ctx.functions?
|
|
|
171
175
|
The HTTP transport can be used with or without the `session` method,
|
|
172
176
|
and unlike the stdio transport it can remain efficient without the
|
|
173
177
|
`session` method through a persistent connection pool that is available
|
|
174
|
-
through the
|
|
178
|
+
through the
|
|
179
|
+
[LLM::Transport.net_http_persistent](https://0x1eef.github.io/x/llm.rb/LLM/Transport.html#method-c-net_http_persistent)
|
|
180
|
+
transport:
|
|
175
181
|
|
|
176
182
|
```ruby
|
|
177
183
|
require "llm"
|
|
@@ -179,7 +185,7 @@ require "llm"
|
|
|
179
185
|
llm = LLM.openai(key: ENV["KEY"])
|
|
180
186
|
mcp = LLM::MCP.http(
|
|
181
187
|
url: "https://remote-mcp.example.com",
|
|
182
|
-
transport:
|
|
188
|
+
transport: :net_http_persistent
|
|
183
189
|
)
|
|
184
190
|
|
|
185
191
|
ctx = LLM::Context.new(llm, tools: mcp.tools)
|
|
@@ -220,7 +226,7 @@ require "llm"
|
|
|
220
226
|
|
|
221
227
|
a2a = LLM::A2A.rest(
|
|
222
228
|
url: "https://remote-agent.example.com",
|
|
223
|
-
transport:
|
|
229
|
+
transport: :net_http_persistent
|
|
224
230
|
)
|
|
225
231
|
```
|
|
226
232
|
|
|
@@ -228,6 +234,27 @@ For more on direct messaging, task operations, push notification
|
|
|
228
234
|
configs, and JSON-RPC, see the
|
|
229
235
|
[LLM::A2A API docs](https://0x1eef.github.io/x/llm.rb/LLM/A2A.html).
|
|
230
236
|
|
|
237
|
+
#### Transports
|
|
238
|
+
|
|
239
|
+
Providers use Ruby's standard library Net::HTTP transport by default.
|
|
240
|
+
You can opt into persistent Net::HTTP connections with `persistent: true`,
|
|
241
|
+
or provide a transport shortcut when you want a different backend.
|
|
242
|
+
`transport: :curb` uses libcurl through the optional `curb` gem.
|
|
243
|
+
|
|
244
|
+
Custom transports can implement the
|
|
245
|
+
[LLM::Transport](https://0x1eef.github.io/x/llm.rb/LLM/Transport.html)
|
|
246
|
+
interface and receive transport-agnostic
|
|
247
|
+
[LLM::Transport::Request](https://0x1eef.github.io/x/llm.rb/LLM/Transport/Request.html)
|
|
248
|
+
objects from providers.
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
require "llm"
|
|
252
|
+
|
|
253
|
+
llm = LLM.openai(key: ENV["KEY"], persistent: true)
|
|
254
|
+
llm = LLM.openai(key: ENV["KEY"], transport: :net_http_persistent)
|
|
255
|
+
llm = LLM.openai(key: ENV["KEY"], transport: :curb)
|
|
256
|
+
```
|
|
257
|
+
|
|
231
258
|
#### Skills
|
|
232
259
|
|
|
233
260
|
Skills are reusable instructions loaded from a `SKILL.md` directory. They let
|
|
@@ -260,6 +287,19 @@ llm = LLM.openai(key: ENV["KEY"])
|
|
|
260
287
|
ReleaseAgent.new(llm, stream: $stdout).talk("Prepare the next release.")
|
|
261
288
|
```
|
|
262
289
|
|
|
290
|
+
A skill can also have its sub-agent inherit the parents tools through the
|
|
291
|
+
`inherit` directive. The `inherit` directive has coverage for the "classic"
|
|
292
|
+
tools (a subclass of [LLM::Tool](https://0x1eef.github.io/x/llm.rb/LLM/Tool.html)),
|
|
293
|
+
MCP tools, and A2A tools that a parent context or agent has access to:
|
|
294
|
+
|
|
295
|
+
```yaml
|
|
296
|
+
---
|
|
297
|
+
name: release
|
|
298
|
+
description: Prepare a release
|
|
299
|
+
tools: inherit
|
|
300
|
+
---
|
|
301
|
+
```
|
|
302
|
+
|
|
263
303
|
#### LLM::Stream
|
|
264
304
|
|
|
265
305
|
The
|
|
@@ -389,7 +429,7 @@ gem install llm.rb
|
|
|
389
429
|
|
|
390
430
|
This example uses [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html)
|
|
391
431
|
directly for an interactive REPL. <br> See the
|
|
392
|
-
[deepdive (web)](https://
|
|
432
|
+
[deepdive (web)](https://llmrb.github.io/llm.rb/) or
|
|
393
433
|
[deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
394
434
|
|
|
395
435
|
```ruby
|
|
@@ -442,7 +482,7 @@ different model from the main context. `token_threshold:` accepts either a
|
|
|
442
482
|
fixed token count or a percentage string like `"90%"`, which resolves
|
|
443
483
|
against the active model context window and triggers compaction once total
|
|
444
484
|
token usage goes over that percentage. See the
|
|
445
|
-
[deepdive (web)](https://
|
|
485
|
+
[deepdive (web)](https://llmrb.github.io/llm.rb/) or
|
|
446
486
|
[deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
447
487
|
|
|
448
488
|
```ruby
|
|
@@ -475,7 +515,7 @@ ctx = LLM::Context.new(
|
|
|
475
515
|
This example uses [`LLM::Stream`](https://0x1eef.github.io/x/llm.rb/LLM/Stream.html)
|
|
476
516
|
with the OpenAI Responses API so reasoning output is streamed separately from
|
|
477
517
|
visible assistant output. See the
|
|
478
|
-
[deepdive (web)](https://
|
|
518
|
+
[deepdive (web)](https://llmrb.github.io/llm.rb/) or
|
|
479
519
|
[deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
480
520
|
|
|
481
521
|
To use the Responses API (OpenAI-specific), initialize a
|
|
@@ -510,7 +550,7 @@ ctx.talk("Solve 17 * 19 and show your work.")
|
|
|
510
550
|
|
|
511
551
|
Need to cancel a stream? llm.rb has you covered through
|
|
512
552
|
[`LLM::Context#interrupt!`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#interrupt-21-instance_method).
|
|
513
|
-
<br> See the [deepdive (web)](https://
|
|
553
|
+
<br> See the [deepdive (web)](https://llmrb.github.io/llm.rb/)
|
|
514
554
|
or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
515
555
|
|
|
516
556
|
```ruby
|
|
@@ -538,7 +578,7 @@ The `plugin :llm` integration wraps
|
|
|
538
578
|
wrappers, its built-in persistence contract is the serialized `data` column,
|
|
539
579
|
while `provider:` resolves a real `LLM::Provider` instance and `context:`
|
|
540
580
|
injects defaults such as `model:`. <br> See the
|
|
541
|
-
[deepdive (web)](https://
|
|
581
|
+
[deepdive (web)](https://llmrb.github.io/llm.rb/) or
|
|
542
582
|
[deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
543
583
|
|
|
544
584
|
```ruby
|
|
@@ -574,7 +614,7 @@ one serialized `data` column. If your app has provider, model, or usage
|
|
|
574
614
|
columns, provide them to llm.rb through `provider:` and `context:` instead of
|
|
575
615
|
relying on reserved wrapper columns.
|
|
576
616
|
|
|
577
|
-
See the [deepdive (web)](https://
|
|
617
|
+
See the [deepdive (web)](https://llmrb.github.io/llm.rb/)
|
|
578
618
|
or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
579
619
|
|
|
580
620
|
```ruby
|
|
@@ -631,7 +671,7 @@ manages tool execution for you. Like `acts_as_llm`, its built-in persistence
|
|
|
631
671
|
contract is one serialized `data` column. If your app has provider or model
|
|
632
672
|
columns, provide them to llm.rb through your hooks and agent DSL.
|
|
633
673
|
|
|
634
|
-
See the [deepdive (web)](https://
|
|
674
|
+
See the [deepdive (web)](https://llmrb.github.io/llm.rb/)
|
|
635
675
|
or [deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
636
676
|
|
|
637
677
|
```ruby
|
|
@@ -689,7 +729,7 @@ This example uses [`LLM::MCP`](https://0x1eef.github.io/x/llm.rb/LLM/MCP.html)
|
|
|
689
729
|
over HTTP so remote GitHub MCP tools run through the same
|
|
690
730
|
`LLM::Context` tool path as local tools. It expects a GitHub token in
|
|
691
731
|
`ENV["GITHUB_PAT"]`. See the
|
|
692
|
-
[deepdive (web)](https://
|
|
732
|
+
[deepdive (web)](https://llmrb.github.io/llm.rb/) or
|
|
693
733
|
[deepdive (markdown)](resources/deepdive.md) for more examples.
|
|
694
734
|
|
|
695
735
|
```ruby
|
|
@@ -710,7 +750,7 @@ ctx.talk(ctx.wait(:call)) while ctx.functions?
|
|
|
710
750
|
|
|
711
751
|
## Resources
|
|
712
752
|
|
|
713
|
-
- [deepdive (web)](https://
|
|
753
|
+
- [deepdive (web)](https://llmrb.github.io/llm.rb/) and
|
|
714
754
|
[deepdive (markdown)](resources/deepdive.md) are the examples guide.
|
|
715
755
|
- [relay](https://github.com/llmrb/relay) shows a real application built on
|
|
716
756
|
top of llm.rb.
|
|
@@ -17,13 +17,14 @@ class LLM::A2A
|
|
|
17
17
|
# @param [String] url The base URL of the A2A agent
|
|
18
18
|
# @param [Hash<String, String>] headers Extra HTTP headers
|
|
19
19
|
# @param [Integer, nil] timeout The timeout in seconds
|
|
20
|
-
# @param [
|
|
20
|
+
# @param [Boolean] persistent Whether to use persistent HTTP connections
|
|
21
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport Override transport
|
|
21
22
|
# @param [String] protocol_version The A2A protocol version header
|
|
22
|
-
def initialize(url:, headers: {}, timeout: nil, transport: nil, protocol_version: "1.0")
|
|
23
|
+
def initialize(url:, headers: {}, timeout: nil, persistent: false, transport: nil, protocol_version: "1.0")
|
|
23
24
|
@uri = URI.parse(url)
|
|
24
25
|
@headers = headers
|
|
25
26
|
@protocol_version = protocol_version
|
|
26
|
-
@transport = resolve_transport(@uri,
|
|
27
|
+
@transport = resolve_transport(host: @uri.host, port: uri.port, ssl: @uri.scheme == "https", timeout:, persistent:, transport:)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
##
|
|
@@ -31,7 +32,7 @@ class LLM::A2A
|
|
|
31
32
|
# @param [String] path The URL path
|
|
32
33
|
# @return [Hash]
|
|
33
34
|
def get(path, accept: "application/json")
|
|
34
|
-
req =
|
|
35
|
+
req = LLM::Transport::Request.get(request_path(path), headers(accept:))
|
|
35
36
|
res = transport.request(req, owner: self)
|
|
36
37
|
parse_response(res)
|
|
37
38
|
end
|
|
@@ -42,7 +43,7 @@ class LLM::A2A
|
|
|
42
43
|
# @param [Hash] body The JSON body
|
|
43
44
|
# @return [Hash]
|
|
44
45
|
def post(path, body, content_type: "application/json", accept: "application/json")
|
|
45
|
-
req =
|
|
46
|
+
req = LLM::Transport::Request.post(request_path(path), headers(content_type:, accept:))
|
|
46
47
|
req.body = LLM.json.dump(body)
|
|
47
48
|
res = transport.request(req, owner: self)
|
|
48
49
|
parse_response(res)
|
|
@@ -53,7 +54,7 @@ class LLM::A2A
|
|
|
53
54
|
# @param [String] path The URL path
|
|
54
55
|
# @return [Hash]
|
|
55
56
|
def delete(path, accept: "application/json")
|
|
56
|
-
req =
|
|
57
|
+
req = LLM::Transport::Request.delete(request_path(path), headers(accept:))
|
|
57
58
|
res = transport.request(req, owner: self)
|
|
58
59
|
parse_response(res)
|
|
59
60
|
end
|
|
@@ -66,7 +67,7 @@ class LLM::A2A
|
|
|
66
67
|
# @yieldparam [LLM::Object] event A stream event
|
|
67
68
|
# @return [void]
|
|
68
69
|
def get_stream(path, &on_event)
|
|
69
|
-
req =
|
|
70
|
+
req = LLM::Transport::Request.get(request_path(path), headers(accept: "text/event-stream"))
|
|
70
71
|
stream(req, &on_event)
|
|
71
72
|
end
|
|
72
73
|
|
|
@@ -79,7 +80,7 @@ class LLM::A2A
|
|
|
79
80
|
# @yieldparam [LLM::Object] event A stream event
|
|
80
81
|
# @return [void]
|
|
81
82
|
def post_stream(path, body, content_type: "application/json", &on_event)
|
|
82
|
-
req =
|
|
83
|
+
req = LLM::Transport::Request.post(request_path(path), headers(content_type:, accept: "text/event-stream"))
|
|
83
84
|
req.body = LLM.json.dump(body)
|
|
84
85
|
stream(req, &on_event)
|
|
85
86
|
end
|
data/lib/llm/a2a.rb
CHANGED
|
@@ -61,8 +61,10 @@ class LLM::A2A
|
|
|
61
61
|
# Extra HTTP headers to include in requests (e.g., Authorization)
|
|
62
62
|
# @param [Integer, nil] timeout
|
|
63
63
|
# The timeout in seconds for HTTP requests
|
|
64
|
-
# @param [
|
|
65
|
-
#
|
|
64
|
+
# @param [Boolean] persistent
|
|
65
|
+
# Whether to use persistent HTTP connections
|
|
66
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
67
|
+
# Optional override with any {LLM::Transport} instance, subclass, or shortcut
|
|
66
68
|
# @param [Symbol] binding
|
|
67
69
|
# The protocol binding to use. One of `:rest` or `:jsonrpc`
|
|
68
70
|
# @param [String] base_path
|
|
@@ -70,7 +72,7 @@ class LLM::A2A
|
|
|
70
72
|
# @param [String] protocol_version
|
|
71
73
|
# The expected A2A protocol version. Defaults to `"1.0"`.
|
|
72
74
|
# @return [LLM::A2A]
|
|
73
|
-
def self.http(url:, headers: {}, timeout: 30, transport: nil, binding: :rest, base_path: "", protocol_version: "1.0")
|
|
75
|
+
def self.http(url:, headers: {}, timeout: 30, persistent: false, transport: nil, binding: :rest, base_path: "", protocol_version: "1.0")
|
|
74
76
|
new(
|
|
75
77
|
binding:,
|
|
76
78
|
base_path:,
|
|
@@ -79,6 +81,7 @@ class LLM::A2A
|
|
|
79
81
|
url:,
|
|
80
82
|
headers:,
|
|
81
83
|
timeout:,
|
|
84
|
+
persistent:,
|
|
82
85
|
transport:,
|
|
83
86
|
protocol_version:
|
|
84
87
|
)
|
|
@@ -90,13 +93,15 @@ class LLM::A2A
|
|
|
90
93
|
# @param [String] url
|
|
91
94
|
# @param [Hash<String, String>] headers
|
|
92
95
|
# @param [Integer, nil] timeout
|
|
93
|
-
# @param [
|
|
96
|
+
# @param [Boolean] persistent
|
|
97
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
94
98
|
# @return [LLM::A2A]
|
|
95
|
-
def self.rest(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
|
|
99
|
+
def self.rest(url:, headers: {}, timeout: 30, persistent: false, transport: nil, base_path: "", protocol_version: "1.0")
|
|
96
100
|
http(
|
|
97
101
|
url:,
|
|
98
102
|
headers:,
|
|
99
103
|
timeout:,
|
|
104
|
+
persistent:,
|
|
100
105
|
transport:,
|
|
101
106
|
binding: :rest,
|
|
102
107
|
base_path:,
|
|
@@ -109,13 +114,15 @@ class LLM::A2A
|
|
|
109
114
|
# @param [String] url
|
|
110
115
|
# @param [Hash<String, String>] headers
|
|
111
116
|
# @param [Integer, nil] timeout
|
|
112
|
-
# @param [
|
|
117
|
+
# @param [Boolean] persistent
|
|
118
|
+
# @param [LLM::Transport, Class, Symbol, nil] transport
|
|
113
119
|
# @return [LLM::A2A]
|
|
114
|
-
def self.jsonrpc(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
|
|
120
|
+
def self.jsonrpc(url:, headers: {}, timeout: 30, persistent: false, transport: nil, base_path: "", protocol_version: "1.0")
|
|
115
121
|
http(
|
|
116
122
|
url:,
|
|
117
123
|
headers:,
|
|
118
124
|
timeout:,
|
|
125
|
+
persistent:,
|
|
119
126
|
transport:,
|
|
120
127
|
binding: :jsonrpc,
|
|
121
128
|
base_path:,
|
data/lib/llm/agent.rb
CHANGED
|
@@ -447,10 +447,13 @@ module LLM
|
|
|
447
447
|
strategy = concurrency || :call
|
|
448
448
|
return wait(strategy) unless @confirm&.any?
|
|
449
449
|
confirmables = @ctx.functions.select { @confirm.include?(_1.name.to_s) }
|
|
450
|
-
results = confirmables.map
|
|
451
|
-
|
|
450
|
+
results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
|
|
451
|
+
@ctx.method(:emit_tool_returns).call(confirmables, results)
|
|
452
|
+
if (@ctx.functions - confirmables).any?
|
|
453
|
+
[*results, *wait(strategy, except: confirmables)]
|
|
454
|
+
else
|
|
455
|
+
results
|
|
452
456
|
end
|
|
453
|
-
@ctx.functions? ? [*results, *wait(strategy)] : results
|
|
454
457
|
end
|
|
455
458
|
|
|
456
459
|
##
|
data/lib/llm/context.rb
CHANGED
|
@@ -193,6 +193,7 @@ module LLM
|
|
|
193
193
|
def talk(prompt, params = {})
|
|
194
194
|
@owner = @llm.request_owner
|
|
195
195
|
compactor.compact!(prompt) if compactor.compact?(prompt)
|
|
196
|
+
repair!(@messages, prompt)
|
|
196
197
|
prompt, params, res = mode == :responses ? respond(prompt, params) : complete(prompt, params)
|
|
197
198
|
self.compacted = false
|
|
198
199
|
role = params[:role] || @llm.user_role
|
|
@@ -302,15 +303,21 @@ module LLM
|
|
|
302
303
|
# without using this argument.
|
|
303
304
|
# Otherwise, this controls how pending functions are resolved directly.
|
|
304
305
|
# Use `:call` for sequential execution without spawning.
|
|
306
|
+
# @param [Array<LLM::Function>] except
|
|
307
|
+
# A list of functions to exclude from the wait
|
|
305
308
|
# @return [Array<LLM::Function::Return>]
|
|
306
|
-
def wait(strategy)
|
|
309
|
+
def wait(strategy, except: [])
|
|
307
310
|
if LLM::Stream === stream && !stream.queue.empty?
|
|
308
311
|
@queue = stream.queue
|
|
309
312
|
@queue.wait
|
|
310
313
|
else
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
tools = except.empty? ? functions : functions - except
|
|
315
|
+
guards = guarded_returns(tools:)
|
|
316
|
+
return guards if guards
|
|
317
|
+
@queue = tools.spawn(strategy)
|
|
318
|
+
returns = @queue.wait
|
|
319
|
+
emit_tool_returns(tools, returns)
|
|
320
|
+
returns
|
|
314
321
|
end
|
|
315
322
|
ensure
|
|
316
323
|
@queue = nil
|
|
@@ -515,10 +522,10 @@ module LLM
|
|
|
515
522
|
##
|
|
516
523
|
# Builds in-band guarded returns when the guard blocks tool work.
|
|
517
524
|
# @api private
|
|
518
|
-
def guarded_returns
|
|
525
|
+
def guarded_returns(tools:)
|
|
519
526
|
warning = guard&.call(self)
|
|
520
527
|
return unless warning
|
|
521
|
-
|
|
528
|
+
tools.map { guarded_return_for(_1, warning) }
|
|
522
529
|
end
|
|
523
530
|
|
|
524
531
|
##
|
|
@@ -566,5 +573,33 @@ module LLM
|
|
|
566
573
|
message: warning
|
|
567
574
|
})
|
|
568
575
|
end
|
|
576
|
+
|
|
577
|
+
##
|
|
578
|
+
# Emits tool return callbacks for directly waited function work.
|
|
579
|
+
# @api private
|
|
580
|
+
def emit_tool_returns(tools, returns)
|
|
581
|
+
return unless LLM::Stream === stream
|
|
582
|
+
returns.each_with_index { |result, index| stream.on_tool_return(tools[index], result) }
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
##
|
|
586
|
+
# Closes assistant tool-call messages that do not have matching tool
|
|
587
|
+
# responses. This can happen when a turn is interrupted while a tool
|
|
588
|
+
# call is streaming or waiting for user confirmation.
|
|
589
|
+
# @param [Array<LLM::Message>] messages
|
|
590
|
+
# @param [Object] prompt
|
|
591
|
+
# @return [void]
|
|
592
|
+
def repair!(messages, prompt)
|
|
593
|
+
message = messages.last
|
|
594
|
+
return unless message&.tool_call?
|
|
595
|
+
returns = self.returns + [*prompt].grep(LLM::Function::Return)
|
|
596
|
+
cancelled = []
|
|
597
|
+
[*message.extra.tool_calls].each do |tool|
|
|
598
|
+
next if returns.any? { _1.id == tool[:id] }
|
|
599
|
+
attrs = {cancelled: true, reason: "function call cancelled"}
|
|
600
|
+
cancelled << LLM::Function::Return.new(tool.id, tool.name, attrs)
|
|
601
|
+
end
|
|
602
|
+
messages << LLM::Message.new(@llm.tool_role, cancelled) unless cancelled.empty?
|
|
603
|
+
end
|
|
569
604
|
end
|
|
570
605
|
end
|
data/lib/llm/function/array.rb
CHANGED
data/lib/llm/function.rb
CHANGED
|
@@ -109,8 +109,35 @@ class LLM::Function
|
|
|
109
109
|
|
|
110
110
|
##
|
|
111
111
|
# Returns function arguments
|
|
112
|
-
# @return [Array, nil]
|
|
113
|
-
|
|
112
|
+
# @return [Hash, Array, LLM::Object, nil]
|
|
113
|
+
attr_reader :arguments
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# Sets function arguments, wrapping them in an LLM::Object
|
|
117
|
+
# @param [Hash, LLM::Object] other
|
|
118
|
+
# @return [void]
|
|
119
|
+
def arguments=(other)
|
|
120
|
+
@arguments = LLM::Object.from(other)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# Compares functions by tool call ID when both sides have one.
|
|
125
|
+
# @param [LLM::Function] other
|
|
126
|
+
# @return [Boolean]
|
|
127
|
+
def ==(other)
|
|
128
|
+
return true if equal?(other)
|
|
129
|
+
return false unless self.class === other
|
|
130
|
+
return false unless id && other.id
|
|
131
|
+
id == other.id
|
|
132
|
+
end
|
|
133
|
+
alias_method :eql?, :==
|
|
134
|
+
|
|
135
|
+
##
|
|
136
|
+
# Returns a hash value compatible with {#==}.
|
|
137
|
+
# @return [Integer]
|
|
138
|
+
def hash
|
|
139
|
+
id ? id.hash : object_id.hash
|
|
140
|
+
end
|
|
114
141
|
|
|
115
142
|
##
|
|
116
143
|
# Returns a tracer, or nil
|
|
@@ -292,6 +319,13 @@ class LLM::Function
|
|
|
292
319
|
@cancelled
|
|
293
320
|
end
|
|
294
321
|
|
|
322
|
+
##
|
|
323
|
+
# Returns true when this function is backed by a skill tool.
|
|
324
|
+
# @return [Boolean]
|
|
325
|
+
def skill?
|
|
326
|
+
@runner.respond_to?(:skill?) and @runner.skill?
|
|
327
|
+
end
|
|
328
|
+
|
|
295
329
|
##
|
|
296
330
|
# Returns true when a function has neither been called nor cancelled
|
|
297
331
|
# @return [Boolean]
|
|
@@ -373,10 +407,10 @@ class LLM::Function
|
|
|
373
407
|
# Returns a Return object with either the function result or error information.
|
|
374
408
|
def call_function
|
|
375
409
|
runner = self.runner
|
|
376
|
-
kwargs =
|
|
410
|
+
kwargs = arguments.respond_to?(:to_h) ? arguments.to_h.transform_keys(&:to_sym) : arguments
|
|
377
411
|
Return.new(id, name, runner.call(**kwargs))
|
|
378
412
|
rescue => ex
|
|
379
|
-
Return.new(id, name,
|
|
413
|
+
Return.new(id, name, {error: true, type: ex.class.name, message: ex.message})
|
|
380
414
|
end
|
|
381
415
|
|
|
382
416
|
def call!
|
data/lib/llm/json_adapter.rb
CHANGED
|
@@ -35,9 +35,15 @@ module LLM
|
|
|
35
35
|
class JSONAdapter::JSON < JSONAdapter
|
|
36
36
|
##
|
|
37
37
|
# @return (see JSONAdapter#dump)
|
|
38
|
-
def self.dump(obj,
|
|
38
|
+
def self.dump(obj, state = nil, **options)
|
|
39
39
|
require "json" unless defined?(::JSON)
|
|
40
|
-
::JSON
|
|
40
|
+
if ::JSON::State === state
|
|
41
|
+
::JSON.generate(obj, state)
|
|
42
|
+
elsif state
|
|
43
|
+
::JSON.dump(obj, state, **options)
|
|
44
|
+
else
|
|
45
|
+
::JSON.dump(obj, **options)
|
|
46
|
+
end
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
##
|