llm.rb 9.0.0 → 11.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -4
  3. data/README.md +194 -42
  4. data/data/anthropic.json +278 -258
  5. data/data/bedrock.json +1288 -1561
  6. data/data/deepseek.json +38 -38
  7. data/data/google.json +656 -579
  8. data/data/openai.json +860 -818
  9. data/data/xai.json +243 -552
  10. data/data/zai.json +168 -168
  11. data/lib/llm/a2a/card/capabilities.rb +41 -0
  12. data/lib/llm/a2a/card/interface.rb +34 -0
  13. data/lib/llm/a2a/card/provider.rb +27 -0
  14. data/lib/llm/a2a/card/skill.rb +68 -0
  15. data/lib/llm/a2a/card.rb +144 -0
  16. data/lib/llm/a2a/error.rb +49 -0
  17. data/lib/llm/a2a/notifications.rb +53 -0
  18. data/lib/llm/a2a/tasks.rb +55 -0
  19. data/lib/llm/a2a/transport/http.rb +131 -0
  20. data/lib/llm/a2a.rb +452 -0
  21. data/lib/llm/active_record/acts_as_agent.rb +20 -9
  22. data/lib/llm/active_record/acts_as_llm.rb +4 -4
  23. data/lib/llm/active_record.rb +1 -6
  24. data/lib/llm/agent.rb +96 -71
  25. data/lib/llm/buffer.rb +1 -2
  26. data/lib/llm/context.rb +77 -50
  27. data/lib/llm/file.rb +7 -0
  28. data/lib/llm/function/call_task.rb +46 -0
  29. data/lib/llm/function.rb +28 -2
  30. data/lib/llm/mcp/transport/http.rb +5 -18
  31. data/lib/llm/mcp/transport/stdio.rb +7 -0
  32. data/lib/llm/mcp.rb +20 -17
  33. data/lib/llm/message.rb +1 -1
  34. data/lib/llm/object/kernel.rb +1 -1
  35. data/lib/llm/provider.rb +9 -9
  36. data/lib/llm/providers/anthropic/stream_parser.rb +2 -2
  37. data/lib/llm/providers/bedrock/stream_parser.rb +2 -2
  38. data/lib/llm/providers/google/stream_parser.rb +2 -2
  39. data/lib/llm/providers/openai/responses/stream_parser.rb +2 -2
  40. data/lib/llm/providers/openai/stream_parser.rb +2 -2
  41. data/lib/llm/response.rb +1 -1
  42. data/lib/llm/schema.rb +11 -0
  43. data/lib/llm/sequel/agent.rb +19 -9
  44. data/lib/llm/sequel/plugin.rb +9 -13
  45. data/lib/llm/stream.rb +11 -36
  46. data/lib/llm/tool/param.rb +1 -8
  47. data/lib/llm/tool.rb +57 -27
  48. data/lib/llm/tracer.rb +1 -1
  49. data/lib/llm/transport/http.rb +1 -1
  50. data/lib/llm/transport/stream_decoder.rb +6 -3
  51. data/lib/llm/transport/utils.rb +35 -0
  52. data/lib/llm/transport.rb +1 -0
  53. data/lib/llm/utils.rb +73 -0
  54. data/lib/llm/version.rb +1 -1
  55. data/lib/llm.rb +24 -4
  56. data/llm.gemspec +16 -1
  57. metadata +29 -5
  58. data/lib/llm/bot.rb +0 -3
  59. data/lib/llm/mcp/transport/http/event_handler.rb +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 197ff330dc5e414f4f9291835fbcdeece4450ee3a8d3748e4f9cf28a46db07b1
4
- data.tar.gz: 3020a4511134f292ed38c6fc826b157f05cc31c722e9fe52692b8b2f705551c7
3
+ metadata.gz: 24c3c2930dd3ab321999075b34ef2e5c6d445fec5c873b00ef071caeef3c1406
4
+ data.tar.gz: 0d6921f20dc327f7c424f7282ff3c76f5073ad3eec7f21d483ee7623e4c782f7
5
5
  SHA512:
6
- metadata.gz: 41f733d7d5b8a329420497f85c289f070f9016cf4d1bfdf5c5e49e274714310f5285e5598d4d27f5daa84cf26e837f311541ac8526bd987d7c7d917eb60eca21
7
- data.tar.gz: f751b3887bd380e8f911106bedf6a0c606bcdc813ea4d7b01f2f332311ddd974c6dcfe838c7db5446d1683844ffbf379b2f5c8ef4b94459bedefaafc70be2098
6
+ metadata.gz: 3b96ea3336114822ccb2defee4da43089df6e004cebdf27987562f7f339bc2a733cc169d64f9752cc51d0346a069ba3921e99db69c2de1f795abfa69f260a730
7
+ data.tar.gz: b4135fad5bd1c5499b1177c27d6eb9c2da5a84b50a5492e82fe6396821c2b4a5e0891e55cb557d07e5ee4ec912b7ecdd8c7df223ebe76109aa74b7ede5227195
data/CHANGELOG.md CHANGED
@@ -2,6 +2,184 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v11.0.0
6
+
7
+ Changes since `v10.0.0`.
8
+
9
+ This release removes several deprecated or unused APIs, including the `#chat`
10
+ alias from contexts and agents, the `LLM::Function#register` alias, and the
11
+ unused positional `llm` argument from MCP constructors. Generated MCP and A2A
12
+ tools are no longer added to the global tool registry by default.
13
+
14
+ On the additions side, it introduces the A2A (Agent2Agent) protocol client,
15
+ a new `#ask` convenience interface on contexts and agents, one-shot stdio MCP
16
+ requests outside `#session`, `LLM::Function#def` as a short alias for
17
+ `LLM::Function#define`, `LLM::File#exist?`, and `LLM::Tool.a2a?`.
18
+
19
+ ### Breaking
20
+
21
+ * **Remove the unused `llm` argument from MCP clients** <br>
22
+ Remove the unused positional `llm` argument from `LLM::MCP.new`,
23
+ `LLM::MCP.stdio`, `LLM::MCP.http`, and `LLM.mcp`.
24
+
25
+ * **Stop globally registering generated MCP and A2A tools** <br>
26
+ Generated tools returned by `LLM::Tool.mcp(...)` and
27
+ `LLM::Tool.a2a(...)` are no longer added to the global
28
+ `LLM::Tool.registry` or `LLM::Function.registry`. They still work
29
+ when passed directly to a context or agent, but registry-based lookup
30
+ now only sees normal loaded `LLM::Tool` subclasses.
31
+
32
+ * **Remove `LLM::Function#register`** <br>
33
+ Remove the `LLM::Function#register` alias and prefer
34
+ `LLM::Function#define` or `LLM::Function#def` when binding a
35
+ function to its implementation. The `register` alias was too easy to
36
+ confuse with the class-level `LLM::Tool.register` and
37
+ `LLM::Function.register` registry APIs.
38
+
39
+ * **Remove the `#chat` alias from contexts and agents** <br>
40
+ Remove the `LLM::Context#chat` and `LLM::Agent#chat` aliases. Prefer
41
+ `#talk` for all context and agent turns.
42
+
43
+ ### Add
44
+
45
+ * **Add `LLM::Function#def`** <br>
46
+ Add `LLM::Function#def` as a short alias for
47
+ `LLM::Function#define` when binding a function instance to its
48
+ implementation.
49
+
50
+ * **Add `LLM::MCP#session`** <br>
51
+ Add `LLM::MCP#session` as an alias for `LLM::MCP#run`, and prefer it
52
+ in examples for scoped stdio MCP sessions that should stay alive
53
+ across discovery and tool calls.
54
+
55
+ * **Add `#ask` to contexts and agents** <br>
56
+ Add `LLM::Context#ask` and `LLM::Agent#ask` as a RubyLLM-compatible
57
+ convenience interface over `#talk`. `#ask` accepts a prompt, optional
58
+ `with:` attachments, an optional `stream:` target, and an optional
59
+ block for streamed chunks, and returns an `LLM::Response`.
60
+
61
+ * **Add `LLM::File#exist?`** <br>
62
+ Add `LLM::File#exist?` as a small convenience wrapper for checking
63
+ whether a local file exists on disk.
64
+
65
+ * **Allow one-shot stdio MCP requests outside `#session`** <br>
66
+ Allow `mcp.tools`, `mcp.prompts`, `mcp.find_prompt(...)`, and
67
+ `mcp.call_tool(...)` to work outside `mcp.session` by starting and
68
+ stopping a stdio transport on demand when needed. This makes stdio
69
+ MCP usable without an explicit session block, while keeping
70
+ `mcp.session` as the preferred pattern for efficient, stateful
71
+ stdio workflows.
72
+
73
+ * **Add A2A client support** <br>
74
+ Add `LLM::A2A`, a client for the Agent2Agent (A2A) protocol with
75
+ REST and JSON-RPC bindings. Remote agent skills can be exposed as
76
+ `LLM::Tool` classes and used through `LLM::Context` or `LLM::Agent`,
77
+ and the client also supports direct messaging, streaming, task
78
+ operations, push notification configuration, extended agent cards,
79
+ persistent HTTP transport selection, and optional REST `base_path`
80
+ prefixing.
81
+
82
+ Refactor shared MCP/A2A HTTP transport setup into
83
+ `LLM::Transport::Utils`, and extend
84
+ `LLM::Transport::StreamDecoder` to accept a callback block directly.
85
+
86
+ * **Add `LLM::Tool.a2a?`** <br>
87
+ Add `LLM::Tool.a2a?` and mark generated A2A-backed tool classes so
88
+ callers can distinguish them from local or MCP tools.
89
+
90
+ ### Fix
91
+
92
+ * **Fix context and agent JSON serialization through `LLM.json`** <br>
93
+ Fix `LLM::Context#to_json` and `LLM::Agent#to_json` to serialize
94
+ through `LLM.json.dump(...)` instead of plain `to_json`.
95
+
96
+ * **Fix block-form ORM agent DSL forwarding** <br>
97
+ Fix block-form `model { ... }`, `tools { ... }`, and
98
+ `schema { ... }` declarations in the ActiveRecord and Sequel agent
99
+ wrappers so persisted agent models configure the internal agent class
100
+ the same way as `LLM::Agent`.
101
+
102
+ * **Fix missing `skills` in ORM agent wrappers** <br>
103
+ Fix the ActiveRecord and Sequel agent wrappers to expose `skills`, so
104
+ persisted agent models can declare skills the same way as
105
+ `LLM::Agent`.
106
+
107
+ * **Fix `acts_as_agent#ctx` return type** <br>
108
+ Fix the ActiveRecord `acts_as_agent` wrapper so its `ctx` helper
109
+ returns the wrapped `LLM::Agent` instead of returning the underlying
110
+ `LLM::Context` directly.
111
+
112
+ ## v10.0.0
113
+
114
+ Changes since `v9.0.0`.
115
+
116
+ This release removes the `LLM::Context#respond` method, and
117
+ also removes the deprecated `LLM::Bot` alias. **All** class-level
118
+ agent tunables can now be resolved lazily via a Symbol (method name),
119
+ or a Proc. The `LLM::Agent` class can now confirm a tool call
120
+ before it happens, and the `LLM::Schema` class has been extended
121
+ to support `Array[String,Integer]` as a shorthand for
122
+ `Array[AnyOf[String, Integer]]`. The `LLM::Stream` class has
123
+ had its public method surface reduced to help avoid accidental
124
+ collisions.
125
+
126
+ ### Breaking
127
+
128
+ * **Unify context turns under `#talk`** <br>
129
+ Remove `LLM::Context#respond` and route responses-mode turns through
130
+ `LLM::Context#talk` with `mode: :responses` instead.
131
+
132
+ * **Remove the `LLM::Bot` alias** <br>
133
+ Remove the backward-compatible `LLM::Bot` alias for `LLM::Context`.
134
+ Use `LLM::Context` directly instead.
135
+
136
+ ### Add
137
+
138
+ * **Add shared option resolution through `LLM::Utils`** <br>
139
+ Add `LLM::Utils.resolve_option` for resolving configured values as
140
+ literals, procs, symbol-named methods, or duplicated hashes, and use
141
+ it in agent and ORM option resolution paths.
142
+
143
+ * **Resolve all class-level agent tunables via Proc** <br>
144
+ Let `model`, `tools`, `skills`, `schema`, `stream`, and `tracer`
145
+ declared with a block be lazily evaluated against the agent instance
146
+ at initialization time, matching how `stream` and `tracer` already
147
+ worked.
148
+
149
+ Add `LLM::Agent#params` for direct access to the underlying context
150
+ parameters.
151
+
152
+ Ported from mruby-llm.
153
+
154
+ * **Support `Array[...]` schema and tool param types** <br>
155
+ Let `LLM::Schema` properties and `LLM::Tool` params accept
156
+ `Array[...]` type declarations, including mixed item unions that are
157
+ serialized as `anyOf` array items.
158
+
159
+ * **Add `LLM::Provider#key?`** <br>
160
+ Add `key?` to providers so callers can check whether a non-blank API
161
+ key has been configured.
162
+
163
+ * **Add agent tool confirmation hooks** <br>
164
+ Add `LLM::Agent.confirm` and `LLM::Agent#on_tool_confirmation` so
165
+ selected tools can be approved or cancelled before execution. Pending
166
+ tool resolution now relies on `LLM::Context#functions` so confirmed
167
+ tools are not executed twice when mixed with unconfirmed tool calls.
168
+
169
+ * **Add `LLM::Function#spawn(:call).wait`** <br>
170
+ Add task-shaped sequential execution support for direct
171
+ `LLM::Function#spawn(:call).wait`.
172
+
173
+ ### Fix
174
+
175
+ * **Reduce private internal methods on `LLM::Stream`** <br>
176
+ Remove `tool_not_found` and `__tools__` from `LLM::Stream`. The
177
+ `__tools__` logic is inlined directly into `__find__` since that
178
+ was its only caller. The `tool_not_found` utility method was unused
179
+ externally and added unnecessary surface to LLM::Stream.
180
+
181
+ Ported from mruby-llm.
182
+
5
183
  ## v9.0.0
6
184
 
7
185
  Changes since `v8.1.0`.
@@ -162,7 +340,7 @@ DSML tool-marker filtering in streamed text.
162
340
  blocks that Bedrock rejects.
163
341
 
164
342
  * **Suppress Bedrock DSML tool markers in streamed text** <br>
165
- Filter `"<|DSML|function_calls"` markers out of streamed Bedrock
343
+ Filter `\"<|DSML|function_calls\"` markers out of streamed Bedrock
166
344
  assistant text so tool-call sentinels do not leak into user-visible
167
345
  output.
168
346
 
@@ -313,7 +491,7 @@ provider usage has been recorded yet.
313
491
  buffer API.
314
492
 
315
493
  * **Support percentage compaction token thresholds** <br>
316
- Let `LLM::Compactor` accept `token_threshold:` values like `"90%"` so
494
+ Let `LLM::Compactor` accept `token_threshold:` values like `\"90%\"` so
317
495
  compaction can trigger at a percentage of the active model context
318
496
  window.
319
497
 
@@ -1096,7 +1274,7 @@ Changes since `v4.9.0`.
1096
1274
 
1097
1275
  - Add HTTP transport for MCP with `LLM::MCP::Transport::HTTP` for remote servers
1098
1276
  - Add JSON Schema union types (`any_of`, `all_of`, `one_of`) with parser integration
1099
- - Add JSON Schema type array union support (e.g., `"type\": [\"object\", \"null\"]`)
1277
+ - Add JSON Schema type array union support (e.g., `\"type\": [\"object\", \"null\"]`)
1100
1278
  - Add JSON Schema type inference from `const`, `enum`, or `default` fields
1101
1279
 
1102
1280
  ### Change
@@ -1197,7 +1375,7 @@ Notable merged work in this range includes:
1197
1375
  - `Add rack + websocket example (#130)`
1198
1376
  - `feat(gemspec): add changelog URI (#136)`
1199
1377
  - `feat(function): alias ThreadGroup#wait as ThreadGroup#value (#62)`
1200
- - README and screencast refresh across `#66`, `#67`, `#68`, `#71`, and
1378
+ - README and screencast refresh across `#66`, `#68`, `#71`, and
1201
1379
  `#72`
1202
1380
  - `chore(bot): update deprecation warning from v5.0 to v6.0`
1203
1381
  - `fix(deepseek): tolerate malformed tool arguments`
data/README.md CHANGED
@@ -1,10 +1,18 @@
1
1
  <p align="center">
2
- <a href="llm.rb"><img src="https://github.com/llmrb/llm.rb/raw/main/llm.png" width="200" height="200" border="0" alt="llm.rb"></a>
2
+ <a href="https://github.com/llmrb/llm.rb">
3
+ <img src="https://github.com/llmrb/llm.rb/raw/main/llm.png" width="200" height="200" border="0" alt="llm.rb">
4
+ </a>
3
5
  </p>
4
6
  <p align="center">
5
- <a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
6
- <a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
7
- <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-9.0.0-green.svg?" alt="Version"></a>
7
+ <a href="https://0x1eef.github.io/x/llm.rb?rebuild=1">
8
+ <img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc">
9
+ </a>
10
+ <a href="https://opensource.org/license/0bsd">
11
+ <img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License">
12
+ </a>
13
+ <a href="https://github.com/llmrb/llm.rb/tags">
14
+ <img src="https://img.shields.io/badge/version-11.0.0-green.svg?" alt="Version">
15
+ </a>
8
16
  </p>
9
17
 
10
18
  ## About
@@ -13,8 +21,9 @@ llm.rb is Ruby's most capable AI runtime.
13
21
 
14
22
  It runs on Ruby's standard library by default. loads optional pieces
15
23
  only when needed, and offers a single runtime for providers, agents,
16
- tools, skills, MCP, streaming, files, and persisted state. As a bonus,
17
- llm.rb is also [available for mruby](https://github.com/llmrb/mruby-llm).
24
+ tools, skills, MCP, A2A (Agent2Agent), RAG (vector stores & embeddings),
25
+ streaming, files, and persisted state. As a bonus, llm.rb is also
26
+ [available for mruby](https://github.com/llmrb/mruby-llm).
18
27
 
19
28
  It supports OpenAI, OpenAI-compatible endpoints, Anthropic, Google
20
29
  Gemini, DeepSeek, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp. It
@@ -64,6 +73,36 @@ agent = LLM::Agent.new(llm, stream: $stdout)
64
73
  agent.talk "Hello world"
65
74
  ```
66
75
 
76
+ #### Agents (Advanced)
77
+
78
+ An agent can be configured to require confirmation before a tool is
79
+ executed. When a matching tool is called, llm.rb runs
80
+ `on_tool_confirmation`. That callback must decide whether to cancel the
81
+ tool call or approve it and execute it by calling
82
+ `fn.spawn(strategy).wait`, and it must always return an instance of
83
+ [`LLM::Function::Return`](https://0x1eef.github.io/x/llm.rb/LLM/Function/Return.html):
84
+
85
+ ```ruby
86
+ require "llm"
87
+
88
+ class Agent < LLM::Agent
89
+ tools DeleteFile
90
+ confirm "delete-file"
91
+
92
+ def on_tool_confirmation(fn, strategy)
93
+ path = fn.arguments["path"] || fn.arguments[:path]
94
+ if path.start_with?("/tmp/")
95
+ fn.spawn(strategy).wait
96
+ else
97
+ fn.cancel(reason: "Deletion requires approval")
98
+ end
99
+ end
100
+ end
101
+
102
+ llm = LLM.openai(key: ENV["KEY"])
103
+ Agent.new(llm, stream: $stdout).talk("Delete /tmp/example.txt.")
104
+ ```
105
+
67
106
  #### Tools
68
107
 
69
108
  The
@@ -96,7 +135,9 @@ or
96
135
  [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html).
97
136
  In this example, the MCP server runs over stdio and
98
137
  [LLM::Context](https://0x1eef.github.io/x/llm.rb/LLM/Context.html)
99
- uses the same tool loop as local tools:
138
+ uses the same tool loop as local tools. For **stdio**, `mcp.session`
139
+ is the preferred pattern because it keeps one MCP session alive across
140
+ discovery and tool calls:
100
141
 
101
142
  ```ruby
102
143
  require "llm"
@@ -104,20 +145,96 @@ require "llm"
104
145
  llm = LLM.openai(key: ENV["KEY"])
105
146
  mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
106
147
 
107
- mcp.run do
148
+ mcp.session do
108
149
  ctx = LLM::Context.new(llm, stream: $stdout, tools: mcp.tools)
109
150
  ctx.talk "Use the available tools to inspect the environment."
110
151
  ctx.talk(ctx.wait(:call)) while ctx.functions?
111
152
  end
112
153
  ```
113
154
 
155
+ MCP can also be used without `session`. Although it works it is generally
156
+ not recommended for the **stdio** transport because it is inefficient
157
+ to start and stop a fresh MCP process for tool discovery and each tool
158
+ call:
159
+
160
+ ```ruby
161
+ require "llm"
162
+
163
+ llm = LLM.openai(key: ENV["KEY"])
164
+ mcp = LLM::MCP.stdio(argv: ["ruby", "server.rb"])
165
+
166
+ ctx = LLM::Context.new(llm, tools: mcp.tools)
167
+ ctx.talk("Use the available tools to inspect the environment.")
168
+ ctx.talk(ctx.wait(:call)) while ctx.functions?
169
+ ```
170
+
171
+ The HTTP transport can be used with or without the `session` method,
172
+ and unlike the stdio transport it can remain efficient without the
173
+ `session` method through a persistent connection pool that is available
174
+ through the [LLM::Transport.net_http_persistent](https://0x1eef.github.io/x/llm.rb/LLM/Transport.html#method-c-net_http_persistent) transport:
175
+
176
+ ```ruby
177
+ require "llm"
178
+
179
+ llm = LLM.openai(key: ENV["KEY"])
180
+ mcp = LLM::MCP.http(
181
+ url: "https://remote-mcp.example.com",
182
+ transport: LLM::Transport.net_http_persistent
183
+ )
184
+
185
+ ctx = LLM::Context.new(llm, tools: mcp.tools)
186
+ ctx.talk("Use the available tools to inspect the environment.")
187
+ ctx.talk(ctx.wait(:call)) while ctx.functions?
188
+ ```
189
+
190
+ #### A2A (Agent 2 Agent)
191
+
192
+ The
193
+ [LLM::A2A](https://0x1eef.github.io/x/llm.rb/LLM/A2A.html)
194
+ object lets llm.rb use skills provided by a remote A2A agent. Those
195
+ skills are exposed through the same runtime as local tools, so you can
196
+ pass them to either
197
+ [LLM::Context](https://0x1eef.github.io/x/llm.rb/LLM/Context.html)
198
+ or
199
+ [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html).
200
+
201
+ Use remote skills as local tools:
202
+
203
+ ```ruby
204
+ require "llm"
205
+
206
+ a2a = LLM::A2A.rest(
207
+ url: "https://remote-agent.example.com",
208
+ headers: {"Authorization" => "Bearer token"}
209
+ )
210
+ llm = LLM.openai(key: ENV["KEY"])
211
+ ctx = LLM::Context.new(llm, tools: a2a.skills)
212
+ ctx.talk "Analyze this CSV and summarize the trends."
213
+ ctx.talk(ctx.wait(:call)) while ctx.functions?
214
+ ```
215
+
216
+ Use persistent HTTP connections:
217
+
218
+ ```ruby
219
+ require "llm"
220
+
221
+ a2a = LLM::A2A.rest(
222
+ url: "https://remote-agent.example.com",
223
+ transport: LLM::Transport.net_http_persistent
224
+ )
225
+ ```
226
+
227
+ For more on direct messaging, task operations, push notification
228
+ configs, and JSON-RPC, see the
229
+ [LLM::A2A API docs](https://0x1eef.github.io/x/llm.rb/LLM/A2A.html).
230
+
114
231
  #### Skills
115
232
 
116
233
  Skills are reusable instructions loaded from a `SKILL.md` directory. They let
117
- you package behavior and tool access together, and they plug into the same
118
- runtime as tools, agents, and MCP. When a skill runs, llm.rb spawns a
119
- subagent with the skill instructions, access to only the tools listed in the
120
- skill, and recent conversation context:
234
+ you package behavior and tool access together, and they plug into the
235
+ same runtime as tools, agents, MCP, and A2A. When a skill runs, llm.rb
236
+ spawns a subagent with the skill instructions, access to only the tools
237
+ listed in the skill, and recent conversation context:
121
238
 
122
239
  ```yaml
123
240
  ---
@@ -239,6 +356,27 @@ ctx2.restore(string:)
239
356
  ctx2.talk "What is my favorite language?"
240
357
  ```
241
358
 
359
+ #### ask
360
+
361
+ [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html)
362
+ also provides `ask`, a convenience interface that is compatible with
363
+ RubyLLM's `ask` method. It accepts a prompt, an optional `with:`
364
+ attachment path or paths, an optional `stream:` target, and an optional
365
+ block that chunks are yielded to. It returns an
366
+ [`LLM::Response`](https://0x1eef.github.io/x/llm.rb/LLM/Response.html),
367
+ so use `.content` when you want the text directly:
368
+
369
+ ```ruby
370
+ require "llm"
371
+
372
+ llm = LLM.openai(key: ENV["KEY"])
373
+ ctx = LLM::Context.new(llm)
374
+
375
+ puts ctx.ask("Hello world").content
376
+ puts ctx.ask("Summarize this document.", with: "README.md").content
377
+ ctx.ask("Stream this reply.") { $stdout << _1 }
378
+ ```
379
+
242
380
  ## Installation
243
381
 
244
382
  ```bash
@@ -249,7 +387,10 @@ gem install llm.rb
249
387
 
250
388
  #### REPL
251
389
 
252
- This example uses [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) directly for an interactive REPL. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
390
+ This example uses [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html)
391
+ directly for an interactive REPL. <br> See the
392
+ [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or
393
+ [deepdive (markdown)](resources/deepdive.md) for more examples.
253
394
 
254
395
  ```ruby
255
396
  require "llm"
@@ -274,17 +415,18 @@ or [`ctx.remote_file(...)`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#r
274
415
  Those tagged objects carry the metadata the provider adapter needs to turn one
275
416
  Ruby prompt into the provider-specific multimodal request schema.
276
417
 
277
- `ctx.local_file(path)` tags a local path as a `:local_file` object around
278
- `LLM.File(path)`. If the model understands that file type, you can include it
279
- directly in the prompt array instead of uploading it first through a provider
280
- Files API:
418
+ If the model understands that file type, you can attach a local file directly
419
+ with `ctx.ask(..., with: path)` instead of uploading it first through a
420
+ provider Files API. Under the hood, llm.rb tags the path as a
421
+ [`ctx.local_file(...)`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#local_file-instance_method)
422
+ object:
281
423
 
282
424
  ```ruby
283
425
  require "llm"
284
426
 
285
427
  llm = LLM.openai(key: ENV["KEY"])
286
428
  ctx = LLM::Context.new(llm)
287
- ctx.talk ["Summarize this document.", ctx.local_file("README.md")]
429
+ puts ctx.ask("Summarize this document.", with: "README.md").content
288
430
  ```
289
431
 
290
432
  #### Context Compaction
@@ -299,7 +441,9 @@ compactor can also use its own `model:` if you want summarization to run on a
299
441
  different model from the main context. `token_threshold:` accepts either a
300
442
  fixed token count or a percentage string like `"90%"`, which resolves
301
443
  against the active model context window and triggers compaction once total
302
- token usage goes over that percentage. See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
444
+ token usage goes over that percentage. See the
445
+ [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or
446
+ [deepdive (markdown)](resources/deepdive.md) for more examples.
303
447
 
304
448
  ```ruby
305
449
  require "llm"
@@ -328,7 +472,15 @@ ctx = LLM::Context.new(
328
472
 
329
473
  #### Reasoning
330
474
 
331
- This example uses [`LLM::Stream`](https://0x1eef.github.io/x/llm.rb/LLM/Stream.html) with the OpenAI Responses API so reasoning output is streamed separately from visible assistant output. See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
475
+ This example uses [`LLM::Stream`](https://0x1eef.github.io/x/llm.rb/LLM/Stream.html)
476
+ with the OpenAI Responses API so reasoning output is streamed separately from
477
+ visible assistant output. See the
478
+ [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or
479
+ [deepdive (markdown)](resources/deepdive.md) for more examples.
480
+
481
+ To use the Responses API (OpenAI-specific), initialize a
482
+ context or agent with `mode: :responses` and keep using
483
+ `talk` for turns.
332
484
 
333
485
  ```ruby
334
486
  require "llm"
@@ -356,7 +508,10 @@ ctx.talk("Solve 17 * 19 and show your work.")
356
508
 
357
509
  #### Request Cancellation
358
510
 
359
- Need to cancel a stream? llm.rb has you covered through [`LLM::Context#interrupt!`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#interrupt-21-instance_method). <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
511
+ Need to cancel a stream? llm.rb has you covered through
512
+ [`LLM::Context#interrupt!`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html#interrupt-21-instance_method).
513
+ <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html)
514
+ or [deepdive (markdown)](resources/deepdive.md) for more examples.
360
515
 
361
516
  ```ruby
362
517
  require "llm"
@@ -377,7 +532,14 @@ worker.join
377
532
 
378
533
  #### Sequel (ORM)
379
534
 
380
- The `plugin :llm` integration wraps [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) on a `Sequel::Model` and keeps tool execution explicit. Like the ActiveRecord wrappers, its built-in persistence contract is the serialized `data` column, while `provider:` resolves a real `LLM::Provider` instance and `context:` injects defaults such as `model:`. <br> See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
535
+ The `plugin :llm` integration wraps
536
+ [`LLM::Context`](https://0x1eef.github.io/x/llm.rb/LLM/Context.html) on a
537
+ `Sequel::Model` and keeps tool execution explicit. Like the ActiveRecord
538
+ wrappers, its built-in persistence contract is the serialized `data` column,
539
+ while `provider:` resolves a real `LLM::Provider` instance and `context:`
540
+ injects defaults such as `model:`. <br> See the
541
+ [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or
542
+ [deepdive (markdown)](resources/deepdive.md) for more examples.
381
543
 
382
544
  ```ruby
383
545
  require "llm"
@@ -412,7 +574,8 @@ one serialized `data` column. If your app has provider, model, or usage
412
574
  columns, provide them to llm.rb through `provider:` and `context:` instead of
413
575
  relying on reserved wrapper columns.
414
576
 
415
- See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
577
+ See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html)
578
+ or [deepdive (markdown)](resources/deepdive.md) for more examples.
416
579
 
417
580
  ```ruby
418
581
  require "llm"
@@ -468,7 +631,8 @@ manages tool execution for you. Like `acts_as_llm`, its built-in persistence
468
631
  contract is one serialized `data` column. If your app has provider or model
469
632
  columns, provide them to llm.rb through your hooks and agent DSL.
470
633
 
471
- See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
634
+ See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html)
635
+ or [deepdive (markdown)](resources/deepdive.md) for more examples.
472
636
 
473
637
  ```ruby
474
638
  require "llm"
@@ -521,7 +685,12 @@ end
521
685
 
522
686
  #### MCP
523
687
 
524
- This example uses [`LLM::MCP`](https://0x1eef.github.io/x/llm.rb/LLM/MCP.html) over HTTP so remote GitHub MCP tools run through the same `LLM::Context` tool path as local tools. It expects a GitHub token in `ENV["GITHUB_PAT"]`. See the [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or [deepdive (markdown)](resources/deepdive.md) for more examples.
688
+ This example uses [`LLM::MCP`](https://0x1eef.github.io/x/llm.rb/LLM/MCP.html)
689
+ over HTTP so remote GitHub MCP tools run through the same
690
+ `LLM::Context` tool path as local tools. It expects a GitHub token in
691
+ `ENV["GITHUB_PAT"]`. See the
692
+ [deepdive (web)](https://0x1eef.github.io/x/llm.rb/file.deepdive.html) or
693
+ [deepdive (markdown)](resources/deepdive.md) for more examples.
525
694
 
526
695
  ```ruby
527
696
  require "llm"
@@ -534,26 +703,9 @@ mcp = LLM::MCP.http(
534
703
  persistent: true
535
704
  )
536
705
 
537
- mcp.start
538
706
  ctx = LLM::Context.new(llm, stream: $stdout, tools: mcp.tools)
539
707
  ctx.talk("Pull information about my GitHub account.")
540
708
  ctx.talk(ctx.wait(:call)) while ctx.functions?
541
- mcp.stop
542
- ```
543
-
544
- For scoped work, `mcp.run do ... end` is shorter and handles cleanup for you:
545
-
546
- ```ruby
547
- mcp = LLM::MCP.http(
548
- url: "https://api.githubcopilot.com/mcp/",
549
- headers: {"Authorization" => "Bearer #{ENV["GITHUB_PAT"]}"},
550
- persistent: true
551
- )
552
- mcp.run do
553
- ctx = LLM::Context.new(llm, stream: $stdout, tools: mcp.tools)
554
- ctx.talk("Pull information about my GitHub account.")
555
- ctx.talk(ctx.wait(:call)) while ctx.functions?
556
- end
557
709
  ```
558
710
 
559
711
  ## Resources