llm.rb 12.6.0 → 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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +387 -0
  3. data/LICENSE +21 -93
  4. data/README.md +46 -155
  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_llm.rb +6 -6
  9. data/lib/llm/agent.rb +62 -23
  10. data/lib/llm/buffer.rb +85 -3
  11. data/lib/llm/compactor/null.rb +19 -0
  12. data/lib/llm/compactor/truncate.rb +80 -0
  13. data/lib/llm/compactor.rb +42 -124
  14. data/lib/llm/context.rb +31 -37
  15. data/lib/llm/contract.rb +4 -25
  16. data/lib/llm/function/array.rb +15 -14
  17. data/lib/llm/function/async/group.rb +54 -0
  18. data/lib/llm/function/async/reactor.rb +48 -0
  19. data/lib/llm/function/async/task.rb +83 -0
  20. data/lib/llm/function/fiber/group.rb +46 -0
  21. data/lib/llm/function/fiber/task.rb +62 -0
  22. data/lib/llm/function/{fork_group.rb → fork/group.rb} +14 -5
  23. data/lib/llm/function/fork/job.rb +2 -2
  24. data/lib/llm/function/fork/task.rb +19 -10
  25. data/lib/llm/function/group.rb +40 -0
  26. data/lib/llm/function/{ractor_group.rb → ractor/group.rb} +13 -5
  27. data/lib/llm/function/ractor/job.rb +9 -3
  28. data/lib/llm/function/ractor/mailbox.rb +2 -0
  29. data/lib/llm/function/ractor/task.rb +23 -15
  30. data/lib/llm/function/{call_group.rb → sequential/group.rb} +12 -8
  31. data/lib/llm/function/sequential/task.rb +49 -0
  32. data/lib/llm/function/task.rb +25 -48
  33. data/lib/llm/function/thread/group.rb +46 -0
  34. data/lib/llm/function/thread/task.rb +60 -0
  35. data/lib/llm/function.rb +54 -64
  36. data/lib/llm/loop_guard.rb +1 -2
  37. data/lib/llm/mcp.rb +22 -0
  38. data/lib/llm/object.rb +2 -1
  39. data/lib/llm/provider.rb +6 -3
  40. data/lib/llm/providers/google.rb +2 -2
  41. data/lib/llm/repl/command.rb +35 -8
  42. data/lib/llm/repl/commands/compact.rb +33 -0
  43. data/lib/llm/repl/input.rb +80 -15
  44. data/lib/llm/repl/markdown/table.rb +76 -0
  45. data/lib/llm/repl/markdown.rb +31 -1
  46. data/lib/llm/repl/status.rb +1 -1
  47. data/lib/llm/repl/stream.rb +10 -3
  48. data/lib/llm/repl/transcript.rb +1 -1
  49. data/lib/llm/repl/walker.rb +46 -0
  50. data/lib/llm/repl.rb +18 -12
  51. data/lib/llm/response.rb +10 -0
  52. data/lib/llm/schema/leaf.rb +5 -0
  53. data/lib/llm/schema/object.rb +11 -5
  54. data/lib/llm/sequel/plugin.rb +6 -6
  55. data/lib/llm/stream.rb +24 -17
  56. data/lib/llm/tool.rb +20 -4
  57. data/lib/llm/tools/chdir.rb +0 -2
  58. data/lib/llm/tools/git.rb +8 -4
  59. data/lib/llm/tools/mkdir.rb +1 -1
  60. data/lib/llm/tools/pwd.rb +0 -2
  61. data/lib/llm/tools/read_file.rb +0 -2
  62. data/lib/llm/tools/rg.rb +8 -4
  63. data/lib/llm/tools/shell.rb +8 -4
  64. data/lib/llm/tools/utils.rb +31 -0
  65. data/lib/llm/version.rb +1 -1
  66. data/lib/llm.rb +25 -5
  67. data/llm.gemspec +3 -3
  68. data/resources/deepdive.md +645 -58
  69. metadata +24 -13
  70. data/lib/llm/function/call_task.rb +0 -46
  71. data/lib/llm/function/fiber_group.rb +0 -105
  72. data/lib/llm/function/task_group.rb +0 -97
  73. 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.
123
+ #### LLM::Schema
136
124
 
137
- ##### REPL: Agent
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.
138
129
 
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.
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.
142
134
 
143
135
  ```ruby
144
- 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
145
142
 
146
- llm = LLM.deepseek(key: ENV["KEY"])
147
- agent = LLM::Agent.new(llm)
148
- agent.repl
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"}
149
147
  ```
150
148
 
151
- ##### REPL: State
152
-
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.
156
-
157
- ```ruby
158
- require "llm"
159
-
160
- llm = LLM.deepseek(key: ENV["KEY"])
161
- agent = LLM::Agent.new(llm)
162
- agent.repl(path: "session.json")
163
- ```
164
-
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)
193
- ```
194
-
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])
165
+ agent = LLM::Agent.new(llm, name: "my-agent")
166
+ agent.repl(path: "agent.json", tools: LLM::Tool.subclasses)
222
167
  ```
223
168
 
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
 
@@ -363,7 +270,8 @@ require "llm/active_record"
363
270
 
364
271
  class Agent < ApplicationRecord
365
272
  acts_as_agent
366
- set instructions: "solve the user's query",
273
+ set name: "my-agent",
274
+ instructions: "solve the user's query",
367
275
  model: "deepseek-v4-pro",
368
276
  tools: [Research, FinalizeResearch, ActOnResearch]
369
277
 
@@ -481,22 +389,5 @@ and resources.
481
389
 
482
390
  ## License
483
391
 
484
- [Business Source License 1.1](./LICENSE)
485
- <br>
486
- Commercial production use requires a commercial license.
487
- <br>
488
- Each version converts to the [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
489
- four years after its first public release.
490
- <br>
491
- Contact [robert@r.uby.dev](mailto:robert@r.uby.dev) for a commercial license.
492
-
493
- ### Waivers
494
-
495
- Waivers are automatically granted for: <br>
496
-
497
- * Personal use
498
- * Students
499
- * Teachers
500
- * Evaluation, development, and testing
501
- * Non-profits and charities
502
- * 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"
@@ -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,10 +23,10 @@ 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
31
  # set model: "gpt-4.1-nano",
31
32
  # instructions: "You are a Linux system admin",
@@ -36,7 +37,21 @@ module LLM
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]
@@ -51,7 +66,8 @@ module LLM
51
66
  #
52
67
  # @example
53
68
  # class AdminAgent < LLM::Agent
54
- # set instructions: "You are a system administrator",
69
+ # set name: "admin",
70
+ # instructions: "You are a system administrator",
55
71
  # model: "gpt-4.1-nano",
56
72
  # tools: [Shell, ReadFile]
57
73
  # end
@@ -78,6 +94,20 @@ module LLM
78
94
  end
79
95
  end
80
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
+
81
111
  ##
82
112
  # Set or get the default model
83
113
  # @param [String, nil] model
@@ -146,9 +176,9 @@ module LLM
146
176
  #
147
177
  # @param [Symbol, Array<Symbol>, nil] concurrency
148
178
  # Controls how pending tool loops are executed:
149
- # - `:call`: sequential calls
179
+ # - `:sequential`: sequential calls
150
180
  # - `:thread`: concurrent threads
151
- # - `:task`: concurrent async tasks
181
+ # - `:async`: concurrent async tasks
152
182
  # - `:fiber`: concurrent scheduler-backed fibers
153
183
  # - `:fork`: forked child processes
154
184
  # - `:ractor`: concurrent Ruby ractors for class-based tools; MCP tools are not supported,
@@ -247,8 +277,8 @@ module LLM
247
277
  # @option params [Symbol, Array<Symbol>, nil] :concurrency Defaults to the agent class concurrency
248
278
  def initialize(llm, params = {})
249
279
  @llm = llm
250
- fields = %i[model skills schema tracer stream tools concurrency instructions confirm]
251
- 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]
252
282
  fields.each do |field|
253
283
  resolvable = params.key?(field) ? params.delete(field) : self.class.public_send(field)
254
284
  resolve_symbol = !%i[concurrency].include?(field)
@@ -265,6 +295,13 @@ module LLM
265
295
  @ctx = LLM::Context.new(llm, {guard: true}.merge(params))
266
296
  end
267
297
 
298
+ ##
299
+ # Returns the agent's name
300
+ # @return [String]
301
+ def name
302
+ @name
303
+ end
304
+
268
305
  ##
269
306
  # Maintain a conversation via the chat completions API.
270
307
  # This method immediately sends a request to the LLM and returns the response.
@@ -299,10 +336,9 @@ module LLM
299
336
 
300
337
  ##
301
338
  # @return [Array<LLM::Function>]
302
- def functions
303
- @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
304
341
  end
305
- alias_method :pending_functions, :functions
306
342
 
307
343
  ##
308
344
  # @see LLM::Context#returns
@@ -435,6 +471,9 @@ module LLM
435
471
  # By default this method disables the tracer for
436
472
  # the duration of the repl session, and restores
437
473
  # it afterwards.
474
+ # @param [String] name
475
+ # The agent's name.
476
+ # Defaults to {LLM::Agent#name}.
438
477
  # @param [String] path
439
478
  # The path to a file where runtime state is read
440
479
  # from, and written to
@@ -446,7 +485,7 @@ module LLM
446
485
  # When true, the tracer is kept alive during the
447
486
  # repl session. Default is false.
448
487
  # @return [void]
449
- def repl(path: nil, tools: [], skills: [], tracer: false, trace: nil)
488
+ def repl(name: self.name, path: nil, tools: [], skills: [], tracer: false, trace: nil)
450
489
  if trace != nil
451
490
  warn "llm.rb: trace option is deprecated, use tracer instead"
452
491
  tracer = trace
@@ -456,7 +495,7 @@ module LLM
456
495
  self.tracer = nil
457
496
  end
458
497
  require_relative "repl" unless defined?(::LLM::Repl)
459
- LLM::Repl.new(agent: self, path:, tools:, skills:).start
498
+ LLM::Repl.new(agent: self, name:, path:, tools:, skills:).start
460
499
  ensure
461
500
  if !tracer
462
501
  self.tracer = previous
@@ -516,7 +555,7 @@ module LLM
516
555
  # @param [Symbol, Array<Symbol>] strategy
517
556
  # The execution strategy that would be used for the tool call.
518
557
  # @return [LLM::Function::Return]
519
- # Return either `fn.spawn(strategy).wait` to approve execution or
558
+ # Return either `fn.task(strategy).wait` to approve execution or
520
559
  # `fn.cancel(...)` to cancel the call.
521
560
  def on_tool_confirmation(fn, strategy)
522
561
  fn.cancel
@@ -554,12 +593,12 @@ module LLM
554
593
  ##
555
594
  # @return [Array<LLM::Function::Return>]
556
595
  def call_functions
557
- strategy = concurrency || :call
596
+ strategy = concurrency || :sequential
558
597
  return wait(strategy) unless @confirm&.any?
559
- confirmables = @ctx.functions.select { @confirm.include?(_1.name.to_s) }
598
+ confirmables = @ctx.pending_functions.select { @confirm.include?(_1.name.to_s) }
560
599
  results = confirmables.map { method(:on_tool_confirmation).call(_1, strategy) }
561
600
  @ctx.method(:emit_tool_returns).call(confirmables, results)
562
- if (@ctx.functions - confirmables).any?
601
+ if (@ctx.pending_functions - confirmables).any?
563
602
  [*results, *wait(strategy, except: confirmables)]
564
603
  else
565
604
  results
@@ -577,13 +616,13 @@ module LLM
577
616
  stream = params[:stream] || @ctx.params[:stream]
578
617
  params[:stream] = LLM::Stream.try(stream, extra: {concurrency:})
579
618
  res = talk.call(apply_instructions(prompt), params)
580
- while @ctx.functions?
619
+ while @ctx.pending_functions?
581
620
  if max
582
621
  max.times do
583
- break unless @ctx.functions?
622
+ break unless @ctx.pending_functions?
584
623
  res = talk.call(call_functions, params)
585
624
  end
586
- 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?
587
626
  else
588
627
  res = talk.call(call_functions, params)
589
628
  end
data/lib/llm/buffer.rb CHANGED
@@ -3,10 +3,24 @@
3
3
  module LLM
4
4
  ##
5
5
  # {LLM::Buffer LLM::Buffer} provides an Enumerable object that
6
- # tracks messages in a conversation thread.
6
+ # tracks messages in a conversation thread. Access it through
7
+ # {LLM::Context#messages}.
8
+ #
9
+ # @example Working with message history
10
+ # ctx.messages.last # => most recent message
11
+ # ctx.messages.first # => oldest message
12
+ # ctx.messages.select! { |m| m.assistant? }
13
+ # ctx.messages.reverse # => reversed copy
14
+ # ctx.messages.reject! { |m| m.compaction? }
15
+ #
16
+ # @see LLM::Message Individual messages in the buffer
17
+ # @see LLM::Context Where the buffer lives (ctx.messages)
7
18
  class Buffer
8
19
  include Enumerable
9
20
 
21
+ UNDEFINED = Object.new
22
+ private_constant :UNDEFINED
23
+
10
24
  ##
11
25
  # @param [LLM::Provider] provider
12
26
  # @return [LLM::Buffer]
@@ -65,8 +79,69 @@ module LLM
65
79
  # @param [Integer, nil] n
66
80
  # The number of messages to return
67
81
  # @return [LLM::Message, Array<LLM::Message>, nil]
68
- def last(n = nil)
69
- n.nil? ? @messages.last : @messages.last(n)
82
+ def last(n = UNDEFINED)
83
+ n.equal?(UNDEFINED) ? @messages.last : @messages.last(n)
84
+ end
85
+
86
+ ##
87
+ # Returns the first message(s) in the buffer
88
+ # @param [Integer, nil] n
89
+ # The number of messages to return
90
+ # @return [LLM::Message, Array<LLM::Message>, nil]
91
+ def first(n = UNDEFINED)
92
+ n.equal?(UNDEFINED) ? @messages.first : @messages.first(n)
93
+ end
94
+
95
+ ##
96
+ # Removes messages matching the block in-place.
97
+ # @yield [LLM::Message]
98
+ # @return [LLM::Buffer]
99
+ def reject!(&)
100
+ @messages.reject!(&)
101
+ self
102
+ end
103
+ alias_method :delete_if, :reject!
104
+
105
+ ##
106
+ # Keeps messages matching the block in-place.
107
+ # @yield [LLM::Message]
108
+ # @return [LLM::Buffer]
109
+ def select!(&)
110
+ @messages.select!(&)
111
+ self
112
+ end
113
+
114
+ ##
115
+ # Removes and returns the first message.
116
+ # @return [LLM::Message, nil]
117
+ def shift
118
+ @messages.shift
119
+ end
120
+
121
+ ##
122
+ # Removes all messages.
123
+ # @return [LLM::Buffer]
124
+ def clear
125
+ @messages.clear
126
+ self
127
+ end
128
+
129
+ ##
130
+ # Returns all elements after the first n.
131
+ # @param [Integer] n
132
+ # The number of messages to skip
133
+ # @return [Array<LLM::Message>]
134
+ def drop(n)
135
+ @messages.drop(n)
136
+ end
137
+
138
+ ##
139
+ # Returns the first n elements without removing them.
140
+ # @param [Integer] n
141
+ # The number of messages to return
142
+ # @return [Array<LLM::Message>]
143
+ def take(n)
144
+ @messages.take(n)
70
145
  end
71
146
 
72
147
  ##
@@ -103,6 +178,13 @@ module LLM
103
178
  @messages[index]
104
179
  end
105
180
 
181
+ ##
182
+ # Returns a reversed copy of the internal array.
183
+ # @return [Array]
184
+ def reverse
185
+ @messages.reverse
186
+ end
187
+
106
188
  ##
107
189
  # @return [String]
108
190
  def to_json(...)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Compactor
4
+ ##
5
+ # An {LLM::Compactor::Null LLM::Compactor::Null} is a compactor that
6
+ # does nothing. It is used as the default when no compactor strategy
7
+ # is configured.
8
+ #
9
+ # All methods return nil and produce no side effects.
10
+ class Null < self
11
+ ##
12
+ # @param [Hash] opts
13
+ # Ignored
14
+ # @return [nil]
15
+ def call(**opts)
16
+ nil
17
+ end
18
+ end
19
+ end