llm.rb 12.5.1 → 13.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +482 -0
  3. data/LICENSE +21 -93
  4. data/README.md +49 -159
  5. data/data/deepinfra.json +3 -0
  6. data/data/xai.json +1 -1
  7. data/lib/llm/a2a.rb +1 -1
  8. data/lib/llm/active_record/acts_as_agent.rb +32 -0
  9. data/lib/llm/active_record/acts_as_llm.rb +6 -6
  10. data/lib/llm/agent.rb +101 -26
  11. data/lib/llm/buffer.rb +85 -3
  12. data/lib/llm/compactor/null.rb +19 -0
  13. data/lib/llm/compactor/truncate.rb +80 -0
  14. data/lib/llm/compactor.rb +42 -124
  15. data/lib/llm/context.rb +33 -37
  16. data/lib/llm/contract.rb +4 -25
  17. data/lib/llm/function/array.rb +15 -14
  18. data/lib/llm/function/async/group.rb +54 -0
  19. data/lib/llm/function/async/reactor.rb +48 -0
  20. data/lib/llm/function/async/task.rb +83 -0
  21. data/lib/llm/function/fiber/group.rb +46 -0
  22. data/lib/llm/function/fiber/task.rb +62 -0
  23. data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
  24. data/lib/llm/function/fork/job.rb +4 -3
  25. data/lib/llm/function/fork/task.rb +20 -10
  26. data/lib/llm/function/group.rb +40 -0
  27. data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
  28. data/lib/llm/function/ractor/job.rb +19 -3
  29. data/lib/llm/function/ractor/mailbox.rb +9 -0
  30. data/lib/llm/function/ractor/task.rb +24 -15
  31. data/lib/llm/function/{call_group.rb → sequential/group.rb} +17 -8
  32. data/lib/llm/function/sequential/task.rb +49 -0
  33. data/lib/llm/function/task.rb +25 -37
  34. data/lib/llm/function/thread/group.rb +46 -0
  35. data/lib/llm/function/thread/task.rb +60 -0
  36. data/lib/llm/function/tracing.rb +2 -0
  37. data/lib/llm/function.rb +56 -64
  38. data/lib/llm/loop_guard.rb +1 -2
  39. data/lib/llm/mcp.rb +22 -0
  40. data/lib/llm/object.rb +2 -1
  41. data/lib/llm/provider.rb +6 -3
  42. data/lib/llm/providers/google.rb +2 -2
  43. data/lib/llm/repl/command.rb +35 -8
  44. data/lib/llm/repl/commands/compact.rb +33 -0
  45. data/lib/llm/repl/input.rb +80 -15
  46. data/lib/llm/repl/markdown/table.rb +76 -0
  47. data/lib/llm/repl/markdown.rb +31 -1
  48. data/lib/llm/repl/status.rb +1 -1
  49. data/lib/llm/repl/stream.rb +10 -3
  50. data/lib/llm/repl/transcript.rb +1 -1
  51. data/lib/llm/repl/walker.rb +46 -0
  52. data/lib/llm/repl.rb +18 -12
  53. data/lib/llm/response.rb +10 -0
  54. data/lib/llm/schema/leaf.rb +5 -0
  55. data/lib/llm/schema/object.rb +11 -5
  56. data/lib/llm/sequel/agent.rb +32 -0
  57. data/lib/llm/sequel/plugin.rb +6 -6
  58. data/lib/llm/stream.rb +24 -17
  59. data/lib/llm/tool/param.rb +12 -0
  60. data/lib/llm/tool.rb +20 -4
  61. data/lib/llm/tools/chdir.rb +0 -2
  62. data/lib/llm/tools/git.rb +8 -4
  63. data/lib/llm/tools/mkdir.rb +1 -1
  64. data/lib/llm/tools/pwd.rb +0 -2
  65. data/lib/llm/tools/read_file.rb +0 -2
  66. data/lib/llm/tools/rg.rb +8 -4
  67. data/lib/llm/tools/shell.rb +8 -4
  68. data/lib/llm/tools/utils.rb +31 -0
  69. data/lib/llm/version.rb +1 -1
  70. data/lib/llm.rb +25 -5
  71. data/llm.gemspec +3 -3
  72. data/resources/deepdive.md +693 -57
  73. metadata +24 -13
  74. data/lib/llm/function/call_task.rb +0 -46
  75. data/lib/llm/function/fiber_group.rb +0 -105
  76. data/lib/llm/function/task_group.rb +0 -97
  77. data/lib/llm/function/thread_group.rb +0 -102
data/README.md CHANGED
@@ -24,7 +24,8 @@ optional dependencies that are opt-in.
24
24
  The runtime supports OpenAI, OpenAI-compatible endpoints, Anthropic, Google
25
25
  Gemini, Mistral, DeepSeek, DeepInfra, xAI, Z.ai, AWS Bedrock, Ollama, and llama.cpp.
26
26
  It has first-class support for streaming, tool calls, MCP
27
- and A2A, embeddings, vector stores and the RAG pattern.
27
+ and A2A, embeddings, vector stores, OCR, context compaction,
28
+ and the RAG pattern.
28
29
 
29
30
  There are multiple HTTP backends to choose from, tools can be run concurrently
30
31
  or in parallel via threads, async tasks, fibers, ractors, and fork, and it is
@@ -35,6 +36,9 @@ so once you learn the fundamentals, everything else falls into place naturally.
35
36
  you learn llm.rb, you will also be able to use <a href="https://r.uby.dev/mruby-llm">mruby-llm</a> and
36
37
  <a href="https://r.uby.dev/wasm-llm">wasm-llm</a> because the API is pretty much identical.
37
38
 
39
+ For detailed explanations, configuration, and advanced patterns, see the
40
+ [deepdive.md](https://r.uby.dev/llm/deepdive/).
41
+
38
42
  ## Install
39
43
 
40
44
  ```bash
@@ -116,153 +120,52 @@ agent = LLM::Agent.new(llm, stream: MyStream.new)
116
120
  agent.talk "Explain Ruby fibers."
117
121
  ```
118
122
 
119
- #### LLM::REPL
120
-
121
- The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
122
- method allows an agent to spawn a read-eval-print loop
123
- that can be useful while developing or operating agents.
124
- It can be used to debug tool calls, confirm an
125
- agent has done what was expected, or improve an agent by
126
- asking questions about what it has done up to that point.
127
-
128
- This feature requires that the [curses](https://github.com/ruby/curses)
129
- and [kramdown](https://github.com/gettalong/kramdown) libraries are
130
- installed and available to require.
131
-
132
- The TUI displays a status line with a context-usage bar and cost
133
- counter, a scrollable transcript with markdown rendering, and a
134
- multi-line input area. The UI stays responsive while the model
135
- is generating a response.
136
-
137
- ##### REPL: Agent
138
-
139
- A REPL session is started by calling `repl` on any agent
140
- instance. The session inherits the agent's model, tools,
141
- skills, and instructions.
123
+ #### LLM::Schema
142
124
 
143
- ```ruby
144
- require "llm"
145
-
146
- llm = LLM.deepseek(key: ENV["KEY"])
147
- agent = LLM::Agent.new(llm)
148
- agent.repl
149
- ```
150
-
151
- ##### REPL: State
125
+ [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html) subclasses produce typed, structured
126
+ output from any model call. Pass a schema to `LLM::Context#talk`,
127
+ `LLM::Agent#talk`, or `LLM::Provider#complete` to receive validated
128
+ JSON instead of free text. Schemas work alongside tools and streams.
152
129
 
153
- The `path:` option accepts a file path where runtime state
154
- is read from and written to. This lets you resume a
155
- conversation across REPL sessions.
130
+ [`LLM::Schema`](https://r.uby.dev/api-docs/llm.rb/LLM/Schema.html) can define objects, arrays, enums, nested schemas,
131
+ and more. It is also used internally by [`LLM::Tool`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html) for parameter
132
+ definitions, so you already benefit from it when you declare tool
133
+ parameters.
156
134
 
157
135
  ```ruby
158
- require "llm"
136
+ class Weather < LLM::Schema
137
+ property :city, String, "The city name"
138
+ property :temperature, Float, "Current temperature"
139
+ property :conditions, String, "Weather conditions"
140
+ required %i[city temperature conditions]
141
+ end
159
142
 
160
- llm = LLM.deepseek(key: ENV["KEY"])
161
- agent = LLM::Agent.new(llm)
162
- agent.repl(path: "session.json")
143
+ llm = LLM.openai(key: ENV["KEY"])
144
+ agent = LLM::Agent.new(llm, schema: Weather)
145
+ res = agent.talk "Weather in Paris?"
146
+ res.content! # => {city: "Paris", temperature: 15.0, conditions: "Cloudy"}
163
147
  ```
164
148
 
165
- ##### REPL: Tools
166
-
167
- The `tools` option lets you attach additional tools
168
- for the duration of the session. This is in addition to
169
- any tools that might already be associated with an agent.
170
-
171
- A number of optional tools are distributed as part of
172
- llm.rb. They power the agents that can be found in the
173
- [agents/](agents/) directory.
174
-
175
- ```ruby
176
- require "llm"
177
-
178
- llm = LLM.deepseek(key: ENV["KEY"])
179
- agent = LLM::Agent.new(llm)
180
- agent.repl(tools: [Debugger])
181
- ```
149
+ #### LLM::REPL
182
150
 
183
- The following example starts a read-eval-print loop
184
- with all of the builtin tools available.
151
+ The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
152
+ method drops you into a curses-based TUI for talking to an
153
+ agent interactively. The `path:` option saves and restores
154
+ runtime state across sessions. The `tools:` option attaches
155
+ extra tools for the duration of the session. It is like
156
+ `binding.pry` but for agents. For the full reference see the
157
+ [REPL section](https://r.uby.dev/llm/deepdive/#repl) in the
158
+ deepdive.
185
159
 
186
160
  ```ruby
187
161
  require "llm"
188
162
  require "llm/tools"
189
163
 
190
164
  llm = LLM.deepseek(key: ENV["KEY"])
191
- agent = LLM::Agent.new(llm)
192
- agent.repl(tools: LLM::Tool.subclasses)
165
+ agent = LLM::Agent.new(llm, name: "my-agent")
166
+ agent.repl(path: "agent.json", tools: LLM::Tool.subclasses)
193
167
  ```
194
168
 
195
- ##### REPL: Skills
196
-
197
- The `skills` option lets you load extra skill directories
198
- without attaching them to an agent permanently.
199
-
200
- ```ruby
201
- require "llm"
202
-
203
- llm = LLM.deepseek(key: ENV["KEY"])
204
- agent = LLM::Agent.new(llm)
205
- agent.repl(skills: [__dir__])
206
- ```
207
-
208
- ##### REPL: Tracer
209
-
210
- By default the tracer is disabled for the duration of the
211
- session. Setting `tracer: true` configures the REPL to use
212
- the tracer associated with an instance of
213
- [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
214
-
215
- ```ruby
216
- require "llm"
217
-
218
- llm = LLM.deepseek(key: ENV["KEY"])
219
- tracer = LLM.logger(llm, path: "agent.log")
220
- agent = LLM::Agent.new(llm, tracer:)
221
- agent.repl(tracer: true, tools: [Debugger])
222
- ```
223
-
224
- ##### REPL: Commands
225
-
226
- Commands are recognized by a `/` prefix and are backed by the
227
- [`LLM::Repl::Command`](https://r.uby.dev/api-docs/llm.rb/LLM/Repl/Command.html)
228
- class, which can be subclassed to add custom commands. Once you
229
- create a subclass, it is automatically added to the repl. A command
230
- can have zero or more parameters, and all parameters are presumed
231
- to be a String (at least for now).
232
-
233
- ```ruby
234
- require "llm"
235
- require "llm/repl"
236
-
237
- class Greeter < LLM::Command
238
- name "greet"
239
- description "Greets the given name"
240
- parameter :name, String, "The person's name"
241
- required %i[name]
242
-
243
- def call(name:)
244
- write("Welcome #{name}!\n")
245
- end
246
- end
247
- ```
248
-
249
- ##### REPL: Input
250
-
251
- The input area supports several keyboard shortcuts:
252
-
253
- | Key | Action |
254
- |---|---|
255
- | `Enter` | Submit the current prompt |
256
- | `Ctrl+A` | Jump to the start of the line |
257
- | `Ctrl+E` | Jump to the end of the line |
258
- | `Ctrl+F` | Move the cursor forward |
259
- | `Ctrl+K` | Erase from cursor to the end of the line |
260
- | `Ctrl+Y` | Paste previously killed text |
261
- | `Ctrl+D` | Delete the character at the cursor |
262
- | `Left / Right` | Move the cursor |
263
- | `Up / Down` | Scroll the transcript |
264
- | `/exit` | Leave the REPL |
265
-
266
169
  #### LLM::MCP
267
170
 
268
171
  The Model Context Protocol (MCP) has first-class support
@@ -326,16 +229,20 @@ Document.create!(
326
229
 
327
230
  #### Concurrency
328
231
 
329
- The runtime supports five different concurrency strategies that have
232
+ The runtime supports six different concurrency strategies that have
330
233
  different attributes. The choice between all of them often depends
331
234
  on the requirements of your application.
332
235
 
333
- IO-bound tools are a good fit for the `:task`, `:thread`,
236
+ IO-bound tools are a good fit for the `:async`, `:thread`,
334
237
  and `:fiber` strategies while true parallelism can be achieved
335
238
  with the `:fork` and `:ractor` strategies. The
336
- `:fork` strategy also provides a separate process that offers
239
+ `:sequential` strategy runs tools one at a time and is the default.
240
+ The `:fork` strategy also provides a separate process that offers
337
241
  isolation from its parent.
338
242
 
243
+ You can learn more about the llm.rb concurrency model in the
244
+ [deepdive.md](https://r.uby.dev/llm/deepdive/#concurrency).
245
+
339
246
  ```ruby
340
247
  require "llm"
341
248
 
@@ -362,11 +269,11 @@ require "llm"
362
269
  require "llm/active_record"
363
270
 
364
271
  class Agent < ApplicationRecord
365
- acts_as_agent do |agent|
366
- agent.model "deepseek-v4-pro"
367
- agent.instructions "solve the user's query"
368
- agent.tools [Research, FinalizeResearch, ActOnResearch]
369
- end
272
+ acts_as_agent
273
+ set name: "my-agent",
274
+ instructions: "solve the user's query",
275
+ model: "deepseek-v4-pro",
276
+ tools: [Research, FinalizeResearch, ActOnResearch]
370
277
 
371
278
  private
372
279
 
@@ -482,22 +389,5 @@ and resources.
482
389
 
483
390
  ## License
484
391
 
485
- [Business Source License 1.1](./LICENSE)
486
- <br>
487
- Commercial production use requires a commercial license.
488
- <br>
489
- Each version converts to the [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
490
- four years after its first public release.
491
- <br>
492
- Contact [robert@r.uby.dev](mailto:robert@r.uby.dev) for a commercial license.
493
-
494
- ### Waivers
495
-
496
- Waivers are automatically granted for: <br>
497
-
498
- * Personal use
499
- * Students
500
- * Teachers
501
- * Evaluation, development, and testing
502
- * Non-profits and charities
503
- * Companies with less than or equal to 50 employees
392
+ This software is released under the terms of the MIT license. <br>
393
+ See [LICENSE](./LICENSE) for details.
data/data/deepinfra.json CHANGED
@@ -503,6 +503,7 @@
503
503
  "context": 131072,
504
504
  "output": 131072
505
505
  },
506
+ "status": "deprecated",
506
507
  "cost": {
507
508
  "input": 0.4,
508
509
  "output": 0.4
@@ -537,6 +538,7 @@
537
538
  "context": 262144,
538
539
  "output": 65536
539
540
  },
541
+ "status": "deprecated",
540
542
  "cost": {
541
543
  "input": 0.2,
542
544
  "output": 0.8
@@ -832,6 +834,7 @@
832
834
  "context": 196608,
833
835
  "output": 131072
834
836
  },
837
+ "status": "deprecated",
835
838
  "cost": {
836
839
  "input": 0.15,
837
840
  "output": 1.15,
data/data/xai.json CHANGED
@@ -312,7 +312,7 @@
312
312
  "cost": {
313
313
  "input": 2,
314
314
  "output": 6,
315
- "cache_read": 0.5,
315
+ "cache_read": 0.3,
316
316
  "tiers": [
317
317
  {
318
318
  "input": 4,
data/lib/llm/a2a.rb CHANGED
@@ -27,7 +27,7 @@
27
27
  # a2a = LLM::A2A.rest(url: "https://agent.example.com")
28
28
  # ctx = LLM::Context.new(llm, tools: a2a.skills)
29
29
  # ctx.talk("Analyze this data using the remote agent.")
30
- # ctx.talk(ctx.wait(:call)) while ctx.functions?
30
+ # ctx.talk(ctx.wait(:sequential)) while ctx.pending_functions?
31
31
  class LLM::A2A
32
32
  require_relative "a2a/card"
33
33
  require_relative "a2a/error"
@@ -17,9 +17,41 @@ module LLM::ActiveRecord
17
17
  # class and forwarded to an internal agent subclass.
18
18
  module ActsAsAgent
19
19
  module ClassMethods
20
+ ##
21
+ # @return [Class<LLM::Agent>]
20
22
  def agent
21
23
  @agent ||= Class.new(LLM::Agent)
22
24
  end
25
+
26
+ ##
27
+ # Bulk-assign class-level agent defaults.
28
+ #
29
+ # Each key is resolved by calling the corresponding class method on the
30
+ # internal agent subclass.
31
+ #
32
+ # @example
33
+ # class Agent < ApplicationRecord
34
+ # acts_as_agent
35
+ # set instructions: "You are a system administrator",
36
+ # model: "gpt-4.1-nano",
37
+ # tools: [Shell]
38
+ # end
39
+ #
40
+ # @param [Hash] properties
41
+ # @option properties [String] :instructions
42
+ # @option properties [String] :model
43
+ # @option properties [Array<LLM::Function>] :tools
44
+ # @option properties [Array<String>] :skills
45
+ # @option properties [#to_json] :schema
46
+ # @option properties [Symbol, Array<Symbol>] :concurrency
47
+ # @option properties [LLM::Tracer, Proc] :tracer
48
+ # @option properties [Object, Proc] :stream
49
+ # @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
50
+ # @raise [KeyError] when a property key does not match a class-level accessor
51
+ # @return [void]
52
+ def set(properties)
53
+ agent.set(properties)
54
+ end
23
55
  end
24
56
 
25
57
  module Hooks
@@ -104,17 +104,17 @@ module LLM::ActiveRecord
104
104
  end
105
105
 
106
106
  ##
107
- # @see LLM::Context#functions
107
+ # @see LLM::Context#pending_functions
108
108
  # @return [Array<LLM::Function>]
109
- def functions
110
- ctx.functions
109
+ def pending_functions
110
+ ctx.pending_functions
111
111
  end
112
112
 
113
113
  ##
114
- # @see LLM::Context#functions?
114
+ # @see LLM::Context#pending_functions?
115
115
  # @return [Boolean]
116
- def functions?
117
- ctx.functions?
116
+ def pending_functions?
117
+ ctx.pending_functions?
118
118
  end
119
119
 
120
120
  ##
data/lib/llm/agent.rb CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  module LLM
4
4
  ##
5
- # {LLM::Agent LLM::Agent} provides a class-level DSL for defining
6
- # reusable, preconfigured assistants with defaults for model,
7
- # tools, schema, and instructions.
5
+ # {LLM::Agent LLM::Agent} is the recommended entry point for most
6
+ # use-cases. It provides a class-level DSL for defining reusable,
7
+ # preconfigured assistants with defaults for model, tools, schema,
8
+ # and instructions.
8
9
  #
9
10
  # It wraps the same stateful runtime surface as
10
11
  # {LLM::Context LLM::Context}: message history, usage, persistence,
@@ -22,26 +23,91 @@ module LLM
22
23
  # * The default tool attempt budget is `25`. After that, the agent sends
23
24
  # advisory tool errors back through the model and keeps the loop in-band.
24
25
  # Set `tool_attempts: nil` to disable that advisory behavior.
25
- # * Tool loop execution can be configured with `concurrency :call`,
26
- # `:thread`, `:task`, `:fiber`, or `:ractor`.
26
+ # * Tool loop execution can be configured with `concurrency :sequential`,
27
+ # `:thread`, `:async`, `:fiber`, or `:ractor`.
27
28
  #
28
- # @example
29
+ # @example Subclass with defaults
29
30
  # class SystemAdmin < LLM::Agent
30
- # model "gpt-4.1-nano"
31
- # instructions "You are a Linux system admin"
32
- # tools Shell
33
- # schema Result
31
+ # set model: "gpt-4.1-nano",
32
+ # instructions: "You are a Linux system admin",
33
+ # tools: [Shell],
34
+ # schema: Result
34
35
  # end
35
36
  #
36
37
  # llm = LLM.openai(key: ENV["KEY"])
37
38
  # agent = SystemAdmin.new(llm)
38
39
  # agent.talk("Run 'date'")
40
+ #
41
+ # @example Direct instance
42
+ # llm = LLM.deepseek(key: ENV["KEY"])
43
+ # agent = LLM::Agent.new(llm, stream: $stdout)
44
+ # agent.talk "Hello world"
45
+ #
46
+ # @see LLM::Context The low-level runtime that Agent wraps
47
+ # @see LLM::Tool Tools that Agent can call on your behalf
48
+ # @see LLM::Stream Stream callbacks for model output
39
49
  class Agent
50
+ ##
51
+ # @api private
52
+ UNDEFINED = Object.new
53
+ private_constant :UNDEFINED
54
+
40
55
  ##
41
56
  # Returns a provider
42
57
  # @return [LLM::Provider]
43
58
  attr_reader :llm
44
59
 
60
+ ##
61
+ # Bulk-assign class-level agent defaults from a Hash.
62
+ #
63
+ # Each key is resolved by calling the corresponding class method on the
64
+ # agent subclass. An error is raised for unknown keys so that typos are
65
+ # caught early.
66
+ #
67
+ # @example
68
+ # class AdminAgent < LLM::Agent
69
+ # set name: "admin",
70
+ # instructions: "You are a system administrator",
71
+ # model: "gpt-4.1-nano",
72
+ # tools: [Shell, ReadFile]
73
+ # end
74
+ #
75
+ # @param [Hash] properties
76
+ # @option properties [String] :instructions
77
+ # @option properties [String] :model
78
+ # @option properties [Array<LLM::Function>] :tools
79
+ # @option properties [Array<String>] :skills
80
+ # @option properties [#to_json] :schema
81
+ # @option properties [Symbol, Array<Symbol>] :concurrency
82
+ # @option properties [LLM::Tracer, Proc] :tracer
83
+ # @option properties [Object, Proc] :stream
84
+ # @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
85
+ # @raise [KeyError] when a property key does not match a class-level accessor
86
+ # @return [void]
87
+ def self.set(properties)
88
+ properties.each do
89
+ if respond_to?(_1)
90
+ public_send(_1, _2)
91
+ else
92
+ raise KeyError, "key not found: #{_1}"
93
+ end
94
+ end
95
+ end
96
+
97
+ ##
98
+ # Set or get an agent's name
99
+ # @param [String] name
100
+ # The agent name
101
+ # @return [String]
102
+ # Return's the agents name
103
+ def self.name(name = UNDEFINED, &block)
104
+ if name.equal?(UNDEFINED)
105
+ @name || self.to_s.gsub(/(.)([A-Z])/, '\\1-\\2').downcase
106
+ else
107
+ @name = block || name
108
+ end
109
+ end
110
+
45
111
  ##
46
112
  # Set or get the default model
47
113
  # @param [String, nil] model
@@ -110,9 +176,9 @@ module LLM
110
176
  #
111
177
  # @param [Symbol, Array<Symbol>, nil] concurrency
112
178
  # Controls how pending tool loops are executed:
113
- # - `:call`: sequential calls
179
+ # - `:sequential`: sequential calls
114
180
  # - `:thread`: concurrent threads
115
- # - `:task`: concurrent async tasks
181
+ # - `:async`: concurrent async tasks
116
182
  # - `:fiber`: concurrent scheduler-backed fibers
117
183
  # - `:fork`: forked child processes
118
184
  # - `:ractor`: concurrent Ruby ractors for class-based tools; MCP tools are not supported,
@@ -211,8 +277,8 @@ module LLM
211
277
  # @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
212
278
  def initialize(llm, params = {})
213
279
  @llm = llm
214
- fields = %i[model skills schema tracer stream tools concurrency instructions confirm]
215
- fields_ivar = %i[tracer concurrency instructions confirm]
280
+ fields = %i[name model skills schema tracer stream tools concurrency instructions confirm]
281
+ fields_ivar = %i[name tracer concurrency instructions confirm]
216
282
  fields.each do |field|
217
283
  resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
218
284
  resolve_symbol = !%i[concurrency].include?(field)
@@ -229,6 +295,13 @@ module LLM
229
295
  @ctx = LLM::Context.new(llm, {guard: true}.merge(params))
230
296
  end
231
297
 
298
+ ##
299
+ # Returns the agent's name
300
+ # @return [String]
301
+ def name
302
+ @name
303
+ end
304
+
232
305
  ##
233
306
  # Maintain a conversation via the chat completions API.
234
307
  # This method immediately sends a request to the LLM and returns the response.
@@ -263,10 +336,9 @@ module LLM
263
336
 
264
337
  ##
265
338
  # @return [Array<LLM::Function>]
266
- def functions
267
- @tracer ? @llm.with_tracer(@tracer) { @ctx.functions } : @ctx.functions
339
+ def pending_functions
340
+ @tracer ? @llm.with_tracer(@tracer) { @ctx.pending_functions } : @ctx.pending_functions
268
341
  end
269
- alias_method :pending_functions, :functions
270
342
 
271
343
  ##
272
344
  # @see LLM::Context#returns
@@ -399,6 +471,9 @@ module LLM
399
471
  # By default this method disables the tracer for
400
472
  # the duration of the repl session, and restores
401
473
  # it afterwards.
474
+ # @param [String] name
475
+ # The agent's name.
476
+ # Defaults to {LLM::Agent#name}.
402
477
  # @param [String] path
403
478
  # The path to a file where runtime state is read
404
479
  # from, and written to
@@ -410,7 +485,7 @@ module LLM
410
485
  # When true, the tracer is kept alive during the
411
486
  # repl session. Default is false.
412
487
  # @return [void]
413
- def repl(path: nil, tools: [], skills: [], tracer: false, trace: nil)
488
+ def repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
414
489
  if trace != nil
415
490
  warn "llm.rb: trace option is deprecated, use tracer instead"
416
491
  tracer = trace
@@ -420,7 +495,7 @@ module LLM
420
495
  self.tracer = nil
421
496
  end
422
497
  require_relative "repl" unless defined?(::LLM::Repl)
423
- LLM::Repl.new(agent: self, path:, tools:, skills:).start
498
+ LLM::Repl.new(agent: self, name:, path:, tools:, skills:).start
424
499
  ensure
425
500
  if !tracer
426
501
  self.tracer = previous
@@ -480,7 +555,7 @@ module LLM
480
555
  # @param [Symbol, Array<Symbol>] strategy
481
556
  # The execution strategy that would be used for the tool call.
482
557
  # @return [LLM::Function::Return]
483
- # Return either `fn.spawn(strategy).wait` to approve execution or
558
+ # Return either `fn.task(strategy).wait` to approve execution or
484
559
  # `fn.cancel(...)` to cancel the call.
485
560
  def on_tool_confirmation(fn, strategy)
486
561
  fn.cancel
@@ -518,12 +593,12 @@ module LLM
518
593
  ##
519
594
  # @return [Array<LLM::Function::Return>]
520
595
  def call_functions
521
- strategy = concurrency || :call
596
+ strategy = concurrency || :sequential
522
597
  return wait(strategy) unless @confirm&.any?
523
- confirmables = @ctx.functions.select { @confirm.include?(_1.name.to_s) }
598
+ confirmables = @ctx.pending_functions.select { @confirm.include?(_1.name.to_s) }
524
599
  results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
525
600
  @ctx.method(:emit_tool_returns).call(confirmables, results)
526
- if (@ctx.functions - confirmables).any?
601
+ if (@ctx.pending_functions - confirmables).any?
527
602
  [*results, *wait(strategy, except: confirmables)]
528
603
  else
529
604
  results
@@ -541,13 +616,13 @@ module LLM
541
616
  stream = params[:stream] || @ctx.params[:stream]
542
617
  params[:stream] = LLM::Stream.try(stream, extra: {concurrency:})
543
618
  res = talk.call(apply_instructions(prompt), params)
544
- while @ctx.functions?
619
+ while @ctx.pending_functions?
545
620
  if max
546
621
  max.times do
547
- break unless @ctx.functions?
622
+ break unless @ctx.pending_functions?
548
623
  res = talk.call(call_functions, params)
549
624
  end
550
- res = talk.call(@ctx.functions.map(&:rate_limit), params) if @ctx.functions?
625
+ res = talk.call(@ctx.pending_functions.map(&:rate_limit), params) if @ctx.pending_functions?
551
626
  else
552
627
  res = talk.call(call_functions, params)
553
628
  end