llm.rb 13.0.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 +505 -14
- data/README.md +484 -50
- data/bin/llm.rb +148 -0
- 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 +123 -20
- 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/array.rb +3 -3
- 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 +33 -6
- 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 +2 -9
- data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
- 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/mistral.rb +1 -1
- data/lib/llm/providers/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +2 -9
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +7 -9
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +4 -11
- data/lib/llm/repl/bar.rb +4 -3
- data/lib/llm/repl/{transcript.rb → buffer.rb} +69 -29
- data/lib/llm/repl/color.rb +78 -0
- data/lib/llm/repl/command.rb +12 -5
- data/lib/llm/repl/commands/compact.rb +2 -2
- data/lib/llm/repl/commands/help.rb +3 -5
- 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 +11 -3
- data/lib/llm/repl/markdown.rb +34 -8
- data/lib/llm/repl/node.rb +37 -0
- data/lib/llm/repl/status.rb +42 -7
- data/lib/llm/repl/stream.rb +18 -6
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +54 -35
- data/lib/llm/repl.rb +74 -32
- data/lib/llm/skill.rb +20 -4
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +29 -0
- data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
- data/lib/llm/tools/git.rb +3 -0
- data/lib/llm/tools/mkdir.rb +3 -0
- data/lib/llm/tools/rg.rb +3 -0
- data/lib/llm/tools/ruby.rb +46 -0
- data/lib/llm/tools/shell.rb +3 -0
- data/lib/llm/tracer/pretty_logger.rb +127 -0
- data/lib/llm/tracer.rb +1 -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 +9 -2
- 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 +58 -1792
- metadata +51 -7
- data/lib/llm/loop_guard.rb +0 -107
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
## Schema
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
A schema describes the shape of a structured response you expect
|
|
9
|
+
from the model. Instead of parsing free-form text, you declare
|
|
10
|
+
the expected structure and the runtime coerces the response into
|
|
11
|
+
an object with typed accessors.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
When you want a structured response from the model, subclass
|
|
16
|
+
[`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html)
|
|
17
|
+
and declare properties with types and constraints. When you pass
|
|
18
|
+
the schema to
|
|
19
|
+
[`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk)
|
|
20
|
+
or
|
|
21
|
+
[`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk),
|
|
22
|
+
the runtime includes it in the request parameters. The provider returns a
|
|
23
|
+
JSON object matching the schema, which is coerced into an
|
|
24
|
+
[`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html) with typed accessors.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class Estimation < LLM::Schema
|
|
28
|
+
property :age, Integer, "The estimated age of the person"
|
|
29
|
+
property :confidence, Number, "Your confidence in the estimate"
|
|
30
|
+
property :applicable, Boolean, "True when the photo contains a person"
|
|
31
|
+
property :comments, String, "Any additional comments"
|
|
32
|
+
required %i[age confidence applicable comments]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
llm = LLM.openai(key: ENV["KEY"])
|
|
36
|
+
agent = LLM::Agent.new(llm, schema: Estimation)
|
|
37
|
+
res = agent.ask "Given this photo, provide an age estimate", with: "photo.jpg"
|
|
38
|
+
|
|
39
|
+
estimate = res.content!
|
|
40
|
+
|
|
41
|
+
if estimate.applicable
|
|
42
|
+
print "The person is approx ", estimate.age.to_s, " years old"
|
|
43
|
+
else
|
|
44
|
+
print "This photo is not applicable: ", estimate.comments
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Why would I use it?
|
|
49
|
+
|
|
50
|
+
Schemas give you structured data instead of free text. Pass the
|
|
51
|
+
result to other code without parsing. Use them for classification,
|
|
52
|
+
extraction, or any workflow where the output needs to feed into
|
|
53
|
+
another system.
|
|
54
|
+
|
|
55
|
+
#### Notes
|
|
56
|
+
|
|
57
|
+
Schemas can define objects, arrays, enums, and nested schemas.
|
|
58
|
+
They are also used internally by
|
|
59
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
60
|
+
for parameter definitions, so you already benefit from them
|
|
61
|
+
when you declare tool parameters.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
## Skills
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
A skill turns a markdown file into a callable tool. Write the
|
|
9
|
+
instructions in markdown, declare what tools the subagent needs
|
|
10
|
+
in the frontmatter, and register the file with the parent agent.
|
|
11
|
+
When the model calls the skill, the runtime spawns a fresh subagent
|
|
12
|
+
with those instructions as its system prompt and only the tools
|
|
13
|
+
you gave it. The subagent runs one turn and returns. Then it is
|
|
14
|
+
discarded.
|
|
15
|
+
|
|
16
|
+
#### How it works
|
|
17
|
+
|
|
18
|
+
A parent agent works with zero or more skill files. Each skill
|
|
19
|
+
is registered as a tool the model can call.
|
|
20
|
+
|
|
21
|
+
When the model calls a skill, a subagent is created with:
|
|
22
|
+
|
|
23
|
+
- Its own system prompt: the body of the SKILL.md file
|
|
24
|
+
- Limited tools: only what you declare in the frontmatter
|
|
25
|
+
- Context awareness: the last 8 user/assistant messages from the
|
|
26
|
+
parent (tool calls stripped)
|
|
27
|
+
|
|
28
|
+
The subagent runs one turn and returns. The parent sees the result
|
|
29
|
+
like any other tool return.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "llm"
|
|
33
|
+
|
|
34
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
35
|
+
agent = LLM::Agent.new(llm, skills: ["./skills/git-log", "./skills/deploy"])
|
|
36
|
+
agent.talk "Review the recent commits, then deploy if everything looks clean"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Why would I use it?
|
|
40
|
+
|
|
41
|
+
Because each skill spawns a fresh subagent with constrained tools,
|
|
42
|
+
you can build a clear hierarchy. The parent supervises, decides
|
|
43
|
+
which skill to call, inspects the result, and can iterate or
|
|
44
|
+
compose multiple skills.
|
|
45
|
+
|
|
46
|
+
Each skill is focused. A changelog skill gets `git-log` and
|
|
47
|
+
`write-file`, not the shell. A deploy skill gets deployment tools,
|
|
48
|
+
not file readers.
|
|
49
|
+
|
|
50
|
+
Skills are stateless. Every call is independent. No carryover, no
|
|
51
|
+
cross-contamination.
|
|
52
|
+
|
|
53
|
+
#### Notes
|
|
54
|
+
|
|
55
|
+
Skills can also be attached to the REPL session without modifying
|
|
56
|
+
the agent:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
60
|
+
agent = LLM::Agent.new(llm)
|
|
61
|
+
agent.repl(skills: [__dir__])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Frontmatter
|
|
65
|
+
|
|
66
|
+
#### Overview
|
|
67
|
+
|
|
68
|
+
The frontmatter at the top of a skill file controls how the
|
|
69
|
+
subagent sees itself and what it can reach. It sets the skill's
|
|
70
|
+
name (which becomes the tool name), a description (which the
|
|
71
|
+
model reads to decide when to call it), and a tool list (which
|
|
72
|
+
limits what the subagent can do). Leave the tools out and the
|
|
73
|
+
subagent has none. Set them to `all` and it has everything.
|
|
74
|
+
|
|
75
|
+
#### How it works
|
|
76
|
+
|
|
77
|
+
The frontmatter is parsed from the YAML block at the top of the
|
|
78
|
+
file. The markdown body that follows becomes the subagent's system
|
|
79
|
+
prompt. The `tools` field resolves at load time: named tools are
|
|
80
|
+
looked up in the registry, `inherit` copies from the parent context,
|
|
81
|
+
and `all` grabs everything available. The `tools` field accepts four
|
|
82
|
+
forms:
|
|
83
|
+
|
|
84
|
+
| Field | Purpose |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `name` | A short identifier for the skill. Used as the tool name. |
|
|
87
|
+
| `description` | Explains to the model what the skill does. Used as the tool description. |
|
|
88
|
+
| `tools` | Controls what the subagent can call. See below. |
|
|
89
|
+
|
|
90
|
+
| Value | Behavior |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `inherit` | Copies the parent agent's tools, excluding other skills. |
|
|
93
|
+
| `all` or `*` | Every tool in the global registry. |
|
|
94
|
+
| `['name1', 'name2']` | Only the named tools. Raises an error if not found. |
|
|
95
|
+
| (omitted) | No tools at all. |
|
|
96
|
+
|
|
97
|
+
#### Why would I use it?
|
|
98
|
+
|
|
99
|
+
The frontmatter is how you configure what a skill can do and what
|
|
100
|
+
tools it has access to. `inherit` copies the parent's capabilities
|
|
101
|
+
into the skill. An explicit list restricts the subagent to only
|
|
102
|
+
the tools it needs.
|
|
103
|
+
|
|
104
|
+
#### Notes
|
|
105
|
+
|
|
106
|
+
The fewer tools a subagent has, the less it can wander.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
## Stream
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
Streaming delivers model output as it is generated, token by
|
|
9
|
+
token, instead of waiting for the full response. The user sees
|
|
10
|
+
text appear in real time, and the application can act on partial
|
|
11
|
+
results as they arrive.
|
|
12
|
+
|
|
13
|
+
#### How it works
|
|
14
|
+
|
|
15
|
+
The stream target can be any object that responds to `#<<`. Each
|
|
16
|
+
time the provider emits a token, the runtime calls `target <<
|
|
17
|
+
chunk` with the raw content. The tokens arrive in order as the
|
|
18
|
+
model generates them, so the output appears character by character
|
|
19
|
+
instead of all at once. When the response is complete, the stream
|
|
20
|
+
closes and control returns to the caller.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "llm"
|
|
24
|
+
|
|
25
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
26
|
+
agent = LLM::Agent.new(llm, stream: $stdout)
|
|
27
|
+
agent.talk "hello world"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
#### Why would I use it?
|
|
31
|
+
|
|
32
|
+
Streaming lets users see output sooner and cancel mid-response if
|
|
33
|
+
the model goes off course. It also enables progress indicators and
|
|
34
|
+
partial result processing, things that are not possible with a
|
|
35
|
+
single blocking response.
|
|
36
|
+
|
|
37
|
+
#### Notes
|
|
38
|
+
|
|
39
|
+
The IO-like form is equivalent to
|
|
40
|
+
[`LLM::Stream#on_content`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_content)
|
|
41
|
+
and does not include the other hooks. It covers content output
|
|
42
|
+
(piping to stdout or a log file) without the overhead of a full
|
|
43
|
+
subclass. A
|
|
44
|
+
[`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
45
|
+
subclass gives you visibility into tool calls, compaction events,
|
|
46
|
+
and reasoning content.
|
|
47
|
+
|
|
48
|
+
### Callbacks
|
|
49
|
+
|
|
50
|
+
#### Overview
|
|
51
|
+
|
|
52
|
+
A stream subclass provides structured hooks into content, tool
|
|
53
|
+
calls, compaction, and other runtime events. Each hook fires at
|
|
54
|
+
a specific point in the request lifecycle. This gives you
|
|
55
|
+
fine-grained visibility into what the runtime is doing as it
|
|
56
|
+
happens, from the first token to the final tool return.
|
|
57
|
+
|
|
58
|
+
#### How it works
|
|
59
|
+
|
|
60
|
+
When you want to react to specific runtime events, override the
|
|
61
|
+
hooks on a
|
|
62
|
+
[`LLM::Stream`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html)
|
|
63
|
+
subclass.
|
|
64
|
+
[`LLM::Stream#on_content`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_content)
|
|
65
|
+
receives tokens as they arrive.
|
|
66
|
+
[`LLM::Stream#on_tool_call`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_tool_call)
|
|
67
|
+
fires when the model requests a tool.
|
|
68
|
+
[`LLM::Stream#on_tool_return`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_tool_return)
|
|
69
|
+
fires when the tool completes. Compaction hooks
|
|
70
|
+
let you show progress or log what was trimmed.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class MyStream < LLM::Stream
|
|
74
|
+
def on_content(content)
|
|
75
|
+
print content
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_reasoning_content(content)
|
|
79
|
+
warn content
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def on_tool_call(tool)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def on_tool_return(tool, result)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def on_compaction(compactor)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def on_compaction_finish(compactor)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
96
|
+
agent = LLM::Agent.new(llm, stream: MyStream.new)
|
|
97
|
+
agent.talk "Explain Ruby fibers."
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Why would I use it?
|
|
101
|
+
|
|
102
|
+
A stream subclass gives you visibility into more than just content chunks. React to tool
|
|
103
|
+
calls as they happen, show compaction progress, or integrate with
|
|
104
|
+
an existing observability stack.
|
|
105
|
+
|
|
106
|
+
#### Notes
|
|
107
|
+
|
|
108
|
+
The IO-like form is equivalent to
|
|
109
|
+
[`LLM::Stream#on_content`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_content)
|
|
110
|
+
and does not include the other hooks.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
|
|
2
|
+
## Tools
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
A tool is how a model reaches outside its own head. Without tools,
|
|
9
|
+
the model can only produce text. With a tool, it can run a shell
|
|
10
|
+
command, query a database, or fetch a web page. The model decides
|
|
11
|
+
when a tool fits the request; the runtime calls it and sends the
|
|
12
|
+
result back.
|
|
13
|
+
|
|
14
|
+
A tool is a Ruby class with a name, a description, and a
|
|
15
|
+
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
|
|
16
|
+
method. The name tells the model what the tool is called. The
|
|
17
|
+
description tells the model when to use it. The
|
|
18
|
+
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
|
|
19
|
+
method receives the arguments and returns the result. That is the entire
|
|
20
|
+
contract: name, description, parameters, and a method that runs.
|
|
21
|
+
|
|
22
|
+
#### How it works
|
|
23
|
+
|
|
24
|
+
A tool is a subclass of
|
|
25
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
26
|
+
with a name, description, and optional typed parameters. The model sees the name and
|
|
27
|
+
description and decides whether to call it. When it does, the
|
|
28
|
+
runtime serialises the arguments and passes them to
|
|
29
|
+
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call).
|
|
30
|
+
|
|
31
|
+
If
|
|
32
|
+
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
|
|
33
|
+
raises, the runtime rescues it and returns a structured
|
|
34
|
+
error to the model instead. The conversation stays valid. You can
|
|
35
|
+
also handle errors yourself inside
|
|
36
|
+
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
|
|
37
|
+
by rescuing and returning a domain-specific error hash.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
class Shell < LLM::Tool
|
|
41
|
+
name "shell"
|
|
42
|
+
description "execute a shell command"
|
|
43
|
+
parameter :name, String, "the command's name"
|
|
44
|
+
parameter :arguments, Array[String], "One or more arguments"
|
|
45
|
+
required %i[name]
|
|
46
|
+
defaults arguments: []
|
|
47
|
+
|
|
48
|
+
def call(name:, arguments: [])
|
|
49
|
+
out = `#{name.shellescape} #{arguments.map(&:shellescape).join(" ")}`
|
|
50
|
+
{ok: $?.success?, out:}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
55
|
+
agent = LLM::Agent.new(llm, tools: [Shell], stream: $stdout)
|
|
56
|
+
agent.talk "What files are in the current working directory?"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Why would I use it?
|
|
60
|
+
|
|
61
|
+
Tools are how the model interacts with the outside world. Without
|
|
62
|
+
them, the model can only produce text. With tools, it can query
|
|
63
|
+
databases, run shell commands, fetch web pages, or call APIs. The
|
|
64
|
+
tool loop stays alive even when a tool fails. One broken tool does
|
|
65
|
+
not derail the conversation.
|
|
66
|
+
|
|
67
|
+
#### Notes
|
|
68
|
+
|
|
69
|
+
Confirmation gates tools behind explicit approval. List tool names
|
|
70
|
+
in
|
|
71
|
+
[`LLM::Agent#confirm`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#confirm)
|
|
72
|
+
to block execution until you override
|
|
73
|
+
[`LLM::Agent#on_tool_confirmation`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#on_tool_confirmation).
|
|
74
|
+
Confirmation also accepts a Symbol that resolves to an instance
|
|
75
|
+
method, letting the confirmed set change per-instance based on
|
|
76
|
+
runtime conditions.
|
|
77
|
+
|
|
78
|
+
Tool properties can be defined with individual method calls (as shown
|
|
79
|
+
in the How it works section) or with
|
|
80
|
+
[`LLM::Tool.set`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#set-class_method)
|
|
81
|
+
(see the Set subsection). Both approaches work the same way.
|
|
82
|
+
|
|
83
|
+
### Confirmation
|
|
84
|
+
|
|
85
|
+
#### Overview
|
|
86
|
+
|
|
87
|
+
Tools that perform destructive actions can be gated behind explicit
|
|
88
|
+
approval. List their names in
|
|
89
|
+
[`LLM::Agent#confirm`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#confirm)
|
|
90
|
+
to block execution
|
|
91
|
+
until you override
|
|
92
|
+
[`LLM::Agent#on_tool_confirmation`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#on_tool_confirmation).
|
|
93
|
+
The default handler cancels
|
|
94
|
+
the tool. Override it per-agent to prompt the user, log the decision,
|
|
95
|
+
or auto-approve certain tools.
|
|
96
|
+
|
|
97
|
+
#### How it works
|
|
98
|
+
|
|
99
|
+
When you want to override the default approval flow for a gated
|
|
100
|
+
tool, override
|
|
101
|
+
[`LLM::Agent#on_tool_confirmation`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#on_tool_confirmation)
|
|
102
|
+
on the subclass. The method receives the pending function and the
|
|
103
|
+
execution strategy. Call
|
|
104
|
+
[`LLM::Function#task`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#task)
|
|
105
|
+
to execute the tool or
|
|
106
|
+
[`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel)
|
|
107
|
+
to block it. The default handler cancels the call.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class AdminAgent < LLM::Agent
|
|
111
|
+
set confirm: %w[delete destroy shutdown]
|
|
112
|
+
|
|
113
|
+
def on_tool_confirmation(fn, strategy)
|
|
114
|
+
print "Run #{fn.name} with #{fn.arguments}? [y/N] "
|
|
115
|
+
$stdin.gets&.match?(/\Ay\z/i) ? fn.task(strategy).wait : fn.cancel
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Why would I use it?
|
|
121
|
+
|
|
122
|
+
Confirmation prevents the model from running dangerous tools
|
|
123
|
+
without user oversight. You decide the approval flow: a terminal
|
|
124
|
+
prompt, a web socket, a background job queue.
|
|
125
|
+
|
|
126
|
+
#### Notes
|
|
127
|
+
|
|
128
|
+
Confirmation names can be a static array of tool names or a Symbol
|
|
129
|
+
that resolves to an instance method. The Symbol form lets the
|
|
130
|
+
confirmed set change per-instance based on runtime conditions.
|
|
131
|
+
|
|
132
|
+
### Errors
|
|
133
|
+
|
|
134
|
+
#### Overview
|
|
135
|
+
|
|
136
|
+
A tool that raises does not crash the conversation. The runtime
|
|
137
|
+
catches the exception, wraps it into a structured error, and
|
|
138
|
+
returns it to the model. The model can read the error, decide what
|
|
139
|
+
went wrong, and try something else. The tool loop stays alive no
|
|
140
|
+
matter what.
|
|
141
|
+
|
|
142
|
+
#### How it works
|
|
143
|
+
|
|
144
|
+
If
|
|
145
|
+
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
|
|
146
|
+
raises, the runtime returns `{error: true, type: "RuntimeError",
|
|
147
|
+
message: "boom"}` to the model. You can also rescue inside
|
|
148
|
+
[`LLM::Tool#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#call)
|
|
149
|
+
and return your own error shape that gives the model more context.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
class Shell < LLM::Tool
|
|
153
|
+
set name: "shell",
|
|
154
|
+
description: "run a shell command",
|
|
155
|
+
parameters: {
|
|
156
|
+
["name", String, "the command name"],
|
|
157
|
+
["arguments", Array[String], "one or more arguments"]
|
|
158
|
+
},
|
|
159
|
+
required: %i[name],
|
|
160
|
+
defaults: {arguments: []}
|
|
161
|
+
|
|
162
|
+
def call(name:, arguments: [])
|
|
163
|
+
out = `#{name} #{arguments.join(" ")}`
|
|
164
|
+
{ok: $?.success?, out:}
|
|
165
|
+
rescue Errno::ENOENT
|
|
166
|
+
{ok: false, error: "command not found: #{name}"}
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### Why would I use it?
|
|
172
|
+
|
|
173
|
+
Custom error handling gives the model domain-specific detail that
|
|
174
|
+
helps it recover. Instead of a generic "RuntimeError: boom", the
|
|
175
|
+
model sees `{ok: false, error: "command not found: ls"}` and knows
|
|
176
|
+
to correct the command name and try again.
|
|
177
|
+
|
|
178
|
+
#### Notes
|
|
179
|
+
|
|
180
|
+
The principle is the same either way: return something. A tool call
|
|
181
|
+
must complete with a tool response. If you do not return a value and
|
|
182
|
+
you do not raise, the runtime has nothing to send back and the
|
|
183
|
+
conversation is stuck.
|
|
184
|
+
|
|
185
|
+
### Set
|
|
186
|
+
|
|
187
|
+
#### Overview
|
|
188
|
+
|
|
189
|
+
[`LLM::Tool.set`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#set-class_method)
|
|
190
|
+
is an alternative way to define tool properties using a Hash. It
|
|
191
|
+
works the same way as individual method calls and accepts the same
|
|
192
|
+
keys: `name`, `description`, `parameters`, `required`, and
|
|
193
|
+
`defaults`.
|
|
194
|
+
|
|
195
|
+
#### How it works
|
|
196
|
+
|
|
197
|
+
When you want to define tool properties at once, call
|
|
198
|
+
[`LLM::Tool.set`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#set-class_method)
|
|
199
|
+
with a Hash. The keys match the individual method names. The
|
|
200
|
+
`parameters` key accepts the same Array of tuples that the
|
|
201
|
+
individual `parameter` method does.
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
class Shell < LLM::Tool
|
|
205
|
+
set name: "shell",
|
|
206
|
+
description: "execute a shell command",
|
|
207
|
+
parameters: [
|
|
208
|
+
[:name, String, "the command's name"],
|
|
209
|
+
[:arguments, Array[String], "One or more arguments"]
|
|
210
|
+
],
|
|
211
|
+
required: %i[name],
|
|
212
|
+
defaults: {arguments: []}
|
|
213
|
+
|
|
214
|
+
def call(name:, arguments: [])
|
|
215
|
+
out = `#{name.shellescape} #{arguments.map(&:shellescape).join(" ")}`
|
|
216
|
+
{ok: $?.success?, out:}
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### Why would I use it?
|
|
222
|
+
|
|
223
|
+
`set` is useful when you want to keep related properties together.
|
|
224
|
+
Instead of spreading `name`, `description`, `parameters`, and
|
|
225
|
+
`required` across multiple lines, you can group them in a single
|
|
226
|
+
Hash that reads like a configuration block.
|
|
227
|
+
|
|
228
|
+
#### Notes
|
|
229
|
+
|
|
230
|
+
Unknown keys raise `KeyError`, so typos are caught at class load
|
|
231
|
+
time rather than at runtime.
|
|
232
|
+
|
|
233
|
+
### Built-in tools
|
|
234
|
+
|
|
235
|
+
#### Overview
|
|
236
|
+
|
|
237
|
+
llm.rb ships with twelve ready-made tools that cover filesystem,
|
|
238
|
+
search, and shell operations. Load them all with
|
|
239
|
+
`require "llm/tools"`. Each tool is documented in the
|
|
240
|
+
[built-in tools catalog](builtin_tools.md).
|
|
241
|
+
|
|
242
|
+
#### How it works
|
|
243
|
+
|
|
244
|
+
When you want to attach the built-in tools to an agent, require
|
|
245
|
+
the catalog and pass the full set of subclasses as the `tools:`
|
|
246
|
+
option.
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
require "llm"
|
|
250
|
+
require "llm/tools"
|
|
251
|
+
|
|
252
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
253
|
+
agent = LLM::Agent.new(llm, tools: LLM::Tool.subclasses)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Why would I use it?
|
|
257
|
+
|
|
258
|
+
The built-in tools cover the operations a coding or system agent
|
|
259
|
+
needs most. See the
|
|
260
|
+
[built-in tools catalog](builtin_tools.md) for the full reference.
|
|
261
|
+
|
|
262
|
+
#### Notes
|
|
263
|
+
|
|
264
|
+
The tools that spawn subprocesses use the optional `test-cmd.rb`
|
|
265
|
+
gem for process management and interrupt handling.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
## A2A
|
|
3
|
+
|
|
4
|
+
### Introduction
|
|
5
|
+
|
|
6
|
+
#### Overview
|
|
7
|
+
|
|
8
|
+
The Agent-to-Agent (A2A) protocol lets agents communicate directly
|
|
9
|
+
over a network. Unlike MCP, which connects an agent to external
|
|
10
|
+
tools, A2A connects agents to other agents. A remote agent advertises
|
|
11
|
+
its skills through a card. The calling agent loads those skills as
|
|
12
|
+
local tools and calls them the same way it calls any other tool.
|
|
13
|
+
One agent can research, another can code, a third can review.
|
|
14
|
+
|
|
15
|
+
#### How it works
|
|
16
|
+
|
|
17
|
+
The REST transport communicates over standard HTTP and JSON. Both
|
|
18
|
+
transports expose the remote agent's skills as local
|
|
19
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html)
|
|
20
|
+
subclasses.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "llm"
|
|
24
|
+
|
|
25
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
26
|
+
a2a = LLM::A2A.rest(url: "https://agent.example.com")
|
|
27
|
+
agent = LLM::Agent.new(llm, tools: a2a.skills)
|
|
28
|
+
agent.talk "What's happening, fellow agent?"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### Why would I use it?
|
|
32
|
+
|
|
33
|
+
A2A lets you compose autonomous agents that delegate and collaborate.
|
|
34
|
+
One agent researches a topic. Another writes code. A third reviews
|
|
35
|
+
the result. Each agent runs independently and communicates over
|
|
36
|
+
the network.
|
|
37
|
+
|
|
38
|
+
#### Notes
|
|
39
|
+
|
|
40
|
+
An agent's capabilities are advertised through a card. The
|
|
41
|
+
[`LLM::A2A::Card#interfaces`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A/Card.html#interfaces)
|
|
42
|
+
method lists which transports a remote agent supports.
|
|
43
|
+
|
|
44
|
+
##### Persistent connections
|
|
45
|
+
|
|
46
|
+
A persistent connection reuses the same HTTP connection across
|
|
47
|
+
requests to the same A2A agent. Both REST and JSON-RPC transports
|
|
48
|
+
accept the `persistent: true` option. Persistent connections matter
|
|
49
|
+
when an agent makes many requests to the same remote agent in a
|
|
50
|
+
short window, since reusing the connection avoids the handshake and
|
|
51
|
+
teardown overhead of a fresh TCP connection per request. The option
|
|
52
|
+
defaults to `false`; for a single request or an infrequent pattern,
|
|
53
|
+
the overhead is negligible. When you want to avoid reopening a TCP
|
|
54
|
+
connection for every request, pass `persistent: true` to the
|
|
55
|
+
transport constructor. This uses
|
|
56
|
+
[`Net::HTTP::Persistent`](https://github.com/drbrain/net-http-persistent)
|
|
57
|
+
under the hood:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
a2a = LLM::A2A.rest(url: "https://agent.example.com", persistent: true)
|
|
61
|
+
a2a = LLM::A2A.jsonrpc(url: "https://agent.example.com", persistent: true)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### JSON-RPC
|
|
65
|
+
|
|
66
|
+
#### Overview
|
|
67
|
+
|
|
68
|
+
JSON-RPC is an alternative transport for A2A agents. It uses a
|
|
69
|
+
more structured protocol than REST, with request and response
|
|
70
|
+
objects that follow the JSON-RPC 2.0 spec. Some agents advertise
|
|
71
|
+
only JSON-RPC, others advertise both. The
|
|
72
|
+
[`LLM::A2A::Card#interfaces`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A/Card.html#interfaces)
|
|
73
|
+
method lists which transports a remote agent supports.
|
|
74
|
+
|
|
75
|
+
#### How it works
|
|
76
|
+
|
|
77
|
+
When you want to connect to a remote A2A agent using JSON-RPC,
|
|
78
|
+
provide a URL and optional headers. The agent's skill list is
|
|
79
|
+
fetched and translated into
|
|
80
|
+
[`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) subclasses the model can call.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
require "llm"
|
|
84
|
+
|
|
85
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
86
|
+
a2a = LLM::A2A.jsonrpc(url: "https://agent.example.com")
|
|
87
|
+
agent = LLM::Agent.new(llm, tools: a2a.skills)
|
|
88
|
+
agent.talk "What's happening, fellow agent?"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Why would I use it?
|
|
92
|
+
|
|
93
|
+
JSON-RPC provides typed request and response objects that follow
|
|
94
|
+
the 2.0 spec, offering a more structured protocol than REST. Some
|
|
95
|
+
agents advertise only JSON-RPC, while others advertise both. The
|
|
96
|
+
[`LLM::A2A::Card#interfaces`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A/Card.html#interfaces)
|
|
97
|
+
method lists which transports a remote agent supports.
|
|
98
|
+
|
|
99
|
+
#### Notes
|
|
100
|
+
|
|
101
|
+
JSON-RPC request and response objects are typed and follow the 2.0
|
|
102
|
+
spec. The
|
|
103
|
+
[`LLM::A2A.jsonrpc`](https://r.uby.dev/api-docs/llm.rb/LLM/A2A.html#jsonrpc-class_method)
|
|
104
|
+
transport provides structured message envelopes rather than plain
|
|
105
|
+
HTTP.
|
|
106
|
+
|