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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +505 -14
  3. data/README.md +484 -50
  4. data/bin/llm.rb +148 -0
  5. data/data/anthropic.json +206 -263
  6. data/data/bedrock.json +2138 -1860
  7. data/data/deepinfra.json +1003 -624
  8. data/data/deepseek.json +38 -34
  9. data/data/google.json +1079 -371
  10. data/data/mistral.json +448 -368
  11. data/data/moonshot.json +384 -0
  12. data/data/openai.json +974 -1343
  13. data/data/xai.json +154 -126
  14. data/data/zai.json +191 -191
  15. data/lib/llm/agent.rb +123 -20
  16. data/lib/llm/context.rb +71 -88
  17. data/lib/llm/cost.rb +23 -17
  18. data/lib/llm/error.rb +0 -8
  19. data/lib/llm/function/array.rb +3 -3
  20. data/lib/llm/function/async/task.rb +2 -0
  21. data/lib/llm/function/fiber/task.rb +2 -0
  22. data/lib/llm/function/fork/task.rb +2 -0
  23. data/lib/llm/function/ractor/task.rb +2 -0
  24. data/lib/llm/function/sequential/group.rb +4 -1
  25. data/lib/llm/function/sequential/task.rb +1 -1
  26. data/lib/llm/function/task.rb +4 -0
  27. data/lib/llm/function/thread/task.rb +2 -0
  28. data/lib/llm/function.rb +33 -6
  29. data/lib/llm/guard/loop.rb +89 -0
  30. data/lib/llm/guard/null.rb +19 -0
  31. data/lib/llm/guard.rb +61 -0
  32. data/lib/llm/provider.rb +36 -0
  33. data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
  34. data/lib/llm/providers/anthropic.rb +2 -9
  35. data/lib/llm/providers/bedrock/request_adapter.rb +1 -1
  36. data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
  37. data/lib/llm/providers/bedrock.rb +1 -8
  38. data/lib/llm/providers/google/stream_parser.rb +1 -0
  39. data/lib/llm/providers/google.rb +1 -8
  40. data/lib/llm/providers/mistral.rb +1 -1
  41. data/lib/llm/providers/moonshot.rb +76 -0
  42. data/lib/llm/providers/ollama.rb +2 -9
  43. data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
  44. data/lib/llm/providers/openai/responses.rb +7 -9
  45. data/lib/llm/providers/openai/stream_parser.rb +1 -0
  46. data/lib/llm/providers/openai.rb +4 -11
  47. data/lib/llm/repl/bar.rb +4 -3
  48. data/lib/llm/repl/{transcript.rb → buffer.rb} +69 -29
  49. data/lib/llm/repl/color.rb +78 -0
  50. data/lib/llm/repl/command.rb +12 -5
  51. data/lib/llm/repl/commands/compact.rb +2 -2
  52. data/lib/llm/repl/commands/help.rb +3 -5
  53. data/lib/llm/repl/input/char.rb +46 -0
  54. data/lib/llm/repl/input/row.rb +39 -0
  55. data/lib/llm/repl/input.rb +251 -66
  56. data/lib/llm/repl/markdown/table.rb +11 -3
  57. data/lib/llm/repl/markdown.rb +34 -8
  58. data/lib/llm/repl/node.rb +37 -0
  59. data/lib/llm/repl/status.rb +42 -7
  60. data/lib/llm/repl/stream.rb +18 -6
  61. data/lib/llm/repl/walker.rb +3 -2
  62. data/lib/llm/repl/window.rb +54 -35
  63. data/lib/llm/repl.rb +74 -32
  64. data/lib/llm/skill.rb +20 -4
  65. data/lib/llm/stream.rb +8 -7
  66. data/lib/llm/tool.rb +29 -0
  67. data/lib/llm/tools/{swap_text.rb → edit-file.rb} +3 -3
  68. data/lib/llm/tools/git.rb +3 -0
  69. data/lib/llm/tools/mkdir.rb +3 -0
  70. data/lib/llm/tools/rg.rb +3 -0
  71. data/lib/llm/tools/ruby.rb +46 -0
  72. data/lib/llm/tools/shell.rb +3 -0
  73. data/lib/llm/tracer/pretty_logger.rb +127 -0
  74. data/lib/llm/tracer.rb +1 -0
  75. data/lib/llm/transformer/null.rb +21 -0
  76. data/lib/llm/transformer.rb +55 -0
  77. data/lib/llm/version.rb +1 -1
  78. data/lib/llm.rb +12 -2
  79. data/llm.gemspec +9 -2
  80. data/resources/deepdive/advanced/cancellation.md +74 -0
  81. data/resources/deepdive/advanced/compaction.md +83 -0
  82. data/resources/deepdive/advanced/context.md +267 -0
  83. data/resources/deepdive/advanced/guard.md +371 -0
  84. data/resources/deepdive/advanced/tracer.md +180 -0
  85. data/resources/deepdive/advanced/transformer.md +67 -0
  86. data/resources/deepdive/advanced/transports.md +45 -0
  87. data/resources/deepdive/everything_else/audio.md +122 -0
  88. data/resources/deepdive/everything_else/cost.md +99 -0
  89. data/resources/deepdive/everything_else/images.md +89 -0
  90. data/resources/deepdive/everything_else/object.md +108 -0
  91. data/resources/deepdive/everything_else/ocr.md +48 -0
  92. data/resources/deepdive/fundamentals/agents.md +202 -0
  93. data/resources/deepdive/fundamentals/builtin_tools.md +191 -0
  94. data/resources/deepdive/fundamentals/concurrency.md +104 -0
  95. data/resources/deepdive/fundamentals/database.md +449 -0
  96. data/resources/deepdive/fundamentals/embeddings.md +157 -0
  97. data/resources/deepdive/fundamentals/repl.md +87 -0
  98. data/resources/deepdive/fundamentals/schema.md +61 -0
  99. data/resources/deepdive/fundamentals/skills.md +106 -0
  100. data/resources/deepdive/fundamentals/stream.md +110 -0
  101. data/resources/deepdive/fundamentals/tools.md +265 -0
  102. data/resources/deepdive/protocols/a2a.md +106 -0
  103. data/resources/deepdive/protocols/mcp.md +111 -0
  104. data/resources/deepdive.md +58 -1792
  105. metadata +51 -7
  106. 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
+