llm.rb 15.2.0 → 15.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c55a3fa1151afad5720fc33bbe60a71feed5577242e53b4e8d4e2f9962a18ed
4
- data.tar.gz: 3099ef73435ed6d91b003a40e2e5c195a82a8f631418b21e7c8cbce9088a5c76
3
+ metadata.gz: '01569fad6afcf4d422b666778989e7bafd1c6e994b6e2be7801ccb5842a7aa3d'
4
+ data.tar.gz: f60ddf02e1a8405cf393e8c581cce61415f459d3207a7c8dc531cbe4e0e87f27
5
5
  SHA512:
6
- metadata.gz: 8e0d5dd6c98355412a31dcb638081ea9f0553725cbf30aa6598eed5d16cd6d4693b5edfae8c063625ee2f5198b342e9b11850897370404267d291ddae4d747fe
7
- data.tar.gz: 25102554a840fee5046804d17ff91673793603b96ab9ae246a30b7d3192a792e9bd7e384d76ce20abf2d504910330202cb2d4aef0057c7875c1ce3866ccfe177
6
+ metadata.gz: 8d9f88421f209e72e1631cbe3e4119d80af29aa72dc44a1917a6dfcbaa76b3d8014d70afe1c771177f6a755ea2d840ffaf92a850606fdd9d08db5c0d0283d0d6
7
+ data.tar.gz: b9d2417b3d2ce4f54ae5c657791518261332508b7dcfe726f19f81d97a27f329220a81ea2ced411fd15625b11c3964c69e1bcfef5df799f9ee0d16f2342cdd56
data/CHANGELOG.md CHANGED
@@ -17,6 +17,63 @@
17
17
 
18
18
  *No unreleased changes yet. Check back after the next release.*
19
19
 
20
+ ## v15.2.2
21
+
22
+ Changes since `v15.2.1`.
23
+
24
+ This release fixes the `set_provider`, `set_context`, and `set_tracer`
25
+ callbacks installed by `acts_as_llm` and `plugin :llm` so a subclass
26
+ inherits them from its superclass instead of raising.
27
+
28
+ ### Fix
29
+
30
+ * **activerecord, sequel: inherit `set_provider` from a superclass** <br>
31
+ The `set_provider`, `set_context`, and `set_tracer` callbacks installed by
32
+ `acts_as_llm` / `plugin :llm` (and their agent counterparts) are now resolved
33
+ through the normal method lookup chain. Defining a callback on a superclass,
34
+ the usual single-table inheritance setup, no longer raises
35
+ `NotImplementedError`, and a subclass can still override it.
36
+
37
+ ## v15.2.1
38
+
39
+ Changes since `v15.2.0`.
40
+
41
+ This release makes the agent count its tool budget across the whole turn
42
+ and emit tool returns to the stream once that budget is spent, and lets
43
+ `LLM::Function#cancel` carry extra return fields.
44
+
45
+ ### Agent
46
+
47
+ * **agent: count the tool budget across the whole turn** <br>
48
+ [`LLM::Agent.tool_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#tool_budget-class_method)
49
+ now caps the tool calls a turn runs in total. A batch of calls is
50
+ spent as a batch, and a batch that would take the turn past its
51
+ budget is not run at all; once the budget is spent no tool call runs,
52
+ and the agent keeps sending its in-band advisory until the model
53
+ answers without requesting more tools. Previously the agent ran up to
54
+ the budget, then ran further batches after each advisory, so a turn
55
+ could run more tool calls than its budget allowed.
56
+
57
+ * **agent: emit tool returns when the budget is spent** <br>
58
+ Fix a bug where, once a turn's tool budget was spent, the agent passed
59
+ its in-band returns to the model without emitting them to the stream.
60
+ The stream went silent, so a stream that tracked tool-call state left
61
+ the calls in the `call` state. The returns are now emitted through
62
+ [`LLM::Stream#on_tool_return`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_tool_return-instance_method)
63
+ like any other tool return.
64
+
65
+ ### Function
66
+
67
+ * **function: `LLM::Function#cancel` takes extra return fields** <br>
68
+ [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method)
69
+ now accepts keywords beyond `reason:` and merges them into the
70
+ [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html)
71
+ it builds. `LLM::Function#budget_spent` now builds its return through
72
+ `cancel` instead of hand-rolling an error, so a budget-spent return is
73
+ marked `cancelled: true` and carries `action:` and `advice:` hints. A
74
+ callback that receives returns can now tell a cancellation apart from
75
+ an ordinary error.
76
+
20
77
  ## v15.2.0
21
78
 
22
79
  Changes since `v15.1.0`.
data/README.md CHANGED
@@ -19,10 +19,10 @@ on CRuby. It has zero runtime dependencies by default, supports
19
19
  concurrent and parallel tool execution and has a single coherent API
20
20
  that spans 14+ providers.
21
21
 
22
- The easiest way to learn about llm.rb is to ask [the r.uby.dev chatbot](https://r.uby.dev)
22
+ The most effective way to learn about llm.rb is to ask [the r.uby.dev chatbot](https://r.uby.dev)
23
23
  a question. It is connected to the llm.rb GitHub repository, backed by
24
- ActiveRecord and uses the builtin MCP feature to connect to GitHub. All
25
- answers are grounded in the llm.rb source code.
24
+ ActiveRecord and uses the builtin MCP feature to connect to GitHub. The chatbot
25
+ is an llm.rb agent that is deployed with [roda-llm](https://github.com/r-uby-dev/roda-llm#readme).
26
26
 
27
27
  ## Install
28
28
 
@@ -37,9 +37,22 @@ gem install llm.rb
37
37
  The
38
38
  [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html)
39
39
  class is the default high-level interface,
40
- and it is recommended for most use-cases. It manages tool execution
41
- automatically and guards against infinite loops,
42
- manages conversation state, and much more.
40
+ and it is recommended for most use-cases. It manages the tool loop
41
+ and provides configurable features on top of it. For example you can
42
+ manage the tool loop with a retry budget alongside a tool call budget,
43
+ among other features.
44
+
45
+ The runtime is designed to keep the tool loop alive and it will
46
+ avoid exceptions. When an error is encountered in a tool or during
47
+ the lifecycle of an agent it is almost always reported back to the
48
+ model as an in-band error that allows the model to correct course.
49
+
50
+ A lot of care also goes into keeping the tool loop from entering
51
+ an invalid state that would lead to API-level errors. For example,
52
+ when a tool call is interrupted it could leave an unanswered tool
53
+ call that a model will reject on the next turn. The runtime takes
54
+ care of this by pruning orphaned tool calls and ensuring that the
55
+ tool loop always remains valid.
43
56
 
44
57
  ```ruby
45
58
  require "llm"
@@ -249,12 +262,12 @@ end
249
262
  <br>
250
263
 
251
264
  The [LLM::Agent#console](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#console-instance_method)
252
- method drops you into a highly capable interactive console
253
- that is built on top of curses. It can help you debug agents,
254
- test your tools, connect to MCP servers, and even A2A agents.
255
- The console stands out because it connects to the surrounding
256
- runtime and it can be extended by your code. Think of it as
257
- `binding.irb` but for agents.
265
+ method drops you into an interactive console that is built on
266
+ top of curses. It can help you debug agents, test your tools,
267
+ connect to MCP servers, and other A2A agents. The console stands
268
+ out because it connects to the surrounding runtime and it can
269
+ be extended by your code. Think of it as `binding.irb` but
270
+ for agents.
258
271
 
259
272
  ##### Demo
260
273
 
@@ -37,7 +37,7 @@ module LLM::ActiveRecord
37
37
  # acts_as_agent
38
38
  # set instructions: "You are a system administrator",
39
39
  # model: "gpt-4.1-nano",
40
- # tools: [Shell]
40
+ # tools: [LLM::Tool::Exec]
41
41
  # end
42
42
  #
43
43
  # @param [Hash] properties
@@ -220,19 +220,25 @@ module LLM::ActiveRecord
220
220
 
221
221
  ##
222
222
  # @return [LLM::Provider]
223
+ # @raise [NotImplementedError]
224
+ # when neither this model nor one of its ancestors implements the
225
+ # callback
223
226
  def set_provider
227
+ return super if defined?(super)
224
228
  raise NotImplementedError, "implement the set_provider callback"
225
229
  end
226
230
 
227
231
  ##
228
232
  # @return [Hash]
229
233
  def set_context
234
+ return super if defined?(super)
230
235
  EMPTY_HASH.dup
231
236
  end
232
237
 
233
238
  ##
234
239
  # @return [LLM::Tracer]
235
240
  def set_tracer
241
+ return super if defined?(super)
236
242
  nil
237
243
  end
238
244
 
data/lib/llm/agent.rb CHANGED
@@ -22,9 +22,12 @@ module LLM
22
22
  # tool-call patterns and blocks stuck execution before more tool work is
23
23
  # queued.
24
24
  # * The tool loop can be bounded with `tool_budget`. Once the budget is
25
- # spent, the agent sends an in-band advisory message back through the
26
- # model and keeps the loop in-band. By default no budget is set
27
- # (`nil`), so the feature is disabled.
25
+ # spent, no further tool calls are run for that turn: the agent sends an
26
+ # in-band advisory message back through the model instead, and keeps
27
+ # sending it while the model keeps asking for tools. The budget counts
28
+ # tool calls, so a batch of calls is spent as a batch, and a batch that
29
+ # would take the turn past its budget is not run at all. By default no
30
+ # budget is set (`nil`), so the feature is disabled.
28
31
  # * Tool loop execution can be configured with `concurrency :sequential`,
29
32
  # `:thread`, `:async`, `:fiber`, `:fork`, or `:ractor`.
30
33
  #
@@ -32,7 +35,7 @@ module LLM
32
35
  # class SystemAdmin < LLM::Agent
33
36
  # set model: "gpt-4.1-nano",
34
37
  # instructions: "You are a Linux system admin",
35
- # tools: [Shell],
38
+ # tools: [LLM::Tool::Exec],
36
39
  # schema: Result
37
40
  # end
38
41
  #
@@ -99,7 +102,7 @@ module LLM
99
102
  # set name: "admin",
100
103
  # instructions: "You are a system administrator",
101
104
  # model: "gpt-4.1-nano",
102
- # tools: [Shell, ReadFile]
105
+ # tools: [LLM::Tool::Exec, LLM::Tool::ReadFile]
103
106
  # end
104
107
  #
105
108
  # @param [Hash] properties
@@ -345,10 +348,15 @@ module LLM
345
348
  ##
346
349
  # Set or get the maximum number of tool calls
347
350
  # that are allowed in a single turn. Once the
348
- # budget is spent, we will return an in-band
349
- # message that informs the model it has spent
350
- # its tool call budget - and usually a model
351
- # will change course afterwards.
351
+ # budget is spent, no further tool calls are run:
352
+ # we return an in-band message that informs the
353
+ # model it has spent its tool call budget, and
354
+ # keep returning it while the model keeps asking
355
+ # for tools - a model will usually change course
356
+ # afterwards. The budget counts tool calls, so a
357
+ # batch of calls is spent as a batch, and a batch
358
+ # that would take the turn past its budget is not
359
+ # run at all.
352
360
  # @note
353
361
  # By default this feature is disabled
354
362
  # (set to `nil`).
@@ -823,14 +831,21 @@ module LLM
823
831
  stream = params[:stream] || @ctx.params[:stream]
824
832
  params[:stream] = LLM::Stream.try(stream, extra: {concurrency:})
825
833
  res = talk.call(apply_instructions(prompt), params)
834
+ spent = 0
826
835
  while @ctx.pending_functions?
827
- if max
828
- max.times do
829
- break unless @ctx.pending_functions?
830
- res = talk.call(call_functions, params)
831
- end
832
- res = talk.call(@ctx.pending_functions.map(&:budget_spent), params) if @ctx.pending_functions?
836
+ batch = @ctx.pending_functions.size
837
+ if max and spent + batch > max
838
+ ##
839
+ # The budget is spent, so the calls are not run.
840
+ # They still have to be answered though: each one
841
+ # gets an in-band return and, just as important,
842
+ # that return is emitted to the stream.
843
+ tools = @ctx.pending_functions
844
+ returns = tools.map(&:budget_spent)
845
+ @ctx.method(:emit_tool_returns).call(tools, returns)
846
+ res = talk.call(returns, params)
833
847
  else
848
+ spent += batch if max
834
849
  res = talk.call(call_functions, params)
835
850
  end
836
851
  end
@@ -16,7 +16,7 @@ class LLM::Console
16
16
  # row by overwriting its contents repeatedly.
17
17
  class Buffer
18
18
  ##
19
- # @param [LLM::Console] console
19
+ # @param [LLM::Console] repl
20
20
  # An instance of {LLM::Console LLM::Console}.
21
21
  # @return [LLM::Console::Buffer]
22
22
  def initialize(repl)
@@ -168,7 +168,7 @@ class LLM::Console
168
168
  attr_reader :agent
169
169
 
170
170
  ##
171
- # @param [LLM::Console] console
171
+ # @param [LLM::Console] repl
172
172
  # @return [LLM::Console::Command]
173
173
  def initialize(repl)
174
174
  @repl = repl
@@ -55,7 +55,7 @@ class LLM::Console
55
55
  attr_writer :paste
56
56
 
57
57
  ##
58
- # @param [LLM::Console] console
58
+ # @param [LLM::Console] repl
59
59
  # @return [LLM::Console::Input]
60
60
  def initialize(repl, options = {})
61
61
  @repl = repl
@@ -12,7 +12,7 @@ class LLM::Console
12
12
  # @api private
13
13
  class Status
14
14
  ##
15
- # @param [LLM::Console] console
15
+ # @param [LLM::Console] repl
16
16
  # @return [LLM::Console::Status]
17
17
  def initialize(repl)
18
18
  @repl = repl
@@ -13,7 +13,7 @@ class LLM::Console
13
13
  attr_reader :tools
14
14
 
15
15
  ##
16
- # @param [LLM::Console] console
16
+ # @param [LLM::Console] repl
17
17
  # @return [LLM::Console::Stream]
18
18
  def initialize(repl, queue)
19
19
  @repl = repl
@@ -24,8 +24,8 @@ class LLM::Console
24
24
  attr_reader :input
25
25
 
26
26
  ##
27
- # @param [LLM::Console] console
28
- # A read-eval-print loop.
27
+ # @param [LLM::Console] repl
28
+ # A console.
29
29
  # @return [LLM::Console::Window]
30
30
  def initialize(repl)
31
31
  @repl = repl
data/lib/llm/function.rb CHANGED
@@ -294,8 +294,8 @@ class LLM::Function
294
294
  # ctx.talk "I want to run the functions"
295
295
  # ctx.talk ctx.pending_functions.map(&:cancel)
296
296
  # @return [LLM::Function::Return]
297
- def cancel(reason: "function call cancelled")
298
- Return.new(id, name, {cancelled: true, reason:})
297
+ def cancel(reason: "function call cancelled", **extra)
298
+ Return.new(id, name, extra.merge(cancelled: true, reason:))
299
299
  ensure
300
300
  @cancelled = true
301
301
  end
@@ -356,12 +356,11 @@ class LLM::Function
356
356
  # call budget has been spent.
357
357
  # @return [LLM::Function::Return]
358
358
  def budget_spent
359
- LLM::Function::Return.new(id, name, {
360
- error: true,
361
- type: "LLM::BudgetSpentError",
362
- message: "the tool call budget for this turn has been spent. " \
363
- "try to solve the problem with less tool calls."
364
- })
359
+ cancel(
360
+ reason: "you are making too many tool calls",
361
+ action: ["stop requesting tool calls"],
362
+ advice: ["produce a response from tool calls already made", "temporary error that resets on the next turn"]
363
+ )
365
364
  end
366
365
 
367
366
  ##
@@ -6,7 +6,7 @@ class LLM::Registry
6
6
  # metadata (pricing, limits, capabilities, and
7
7
  # modalities).
8
8
  #
9
- # Models are {Comparable} by price: input cost first, then
9
+ # Models are `Comparable` by price: input cost first, then
10
10
  # output cost, so `models.sort` orders them from cheapest to
11
11
  # most expensive.
12
12
  class Model
@@ -52,7 +52,7 @@ module LLM::Sequel
52
52
  # plugin :llm_agent
53
53
  # set instructions: "You are a system administrator",
54
54
  # model: "gpt-4.1-nano",
55
- # tools: [Shell]
55
+ # tools: [LLM::Tool::Exec]
56
56
  # end
57
57
  #
58
58
  # @param [Hash] properties
@@ -320,19 +320,25 @@ module LLM::Sequel
320
320
 
321
321
  ##
322
322
  # @return [LLM::Provider]
323
+ # @raise [NotImplementedError]
324
+ # when neither this model nor one of its ancestors implements the
325
+ # callback
323
326
  def set_provider
327
+ return super if defined?(super)
324
328
  raise NotImplementedError, "implement the set_provider callback"
325
329
  end
326
330
 
327
331
  ##
328
332
  # @return [Hash]
329
333
  def set_context
334
+ return super if defined?(super)
330
335
  Plugin::EMPTY_HASH.dup
331
336
  end
332
337
 
333
338
  ##
334
339
  # @return [LLM::Tracer]
335
340
  def set_tracer
341
+ return super if defined?(super)
336
342
  nil
337
343
  end
338
344
 
@@ -46,10 +46,8 @@ class LLM::Tool
46
46
  end
47
47
 
48
48
  ##
49
- # @param [String] name
50
- # The name of a command
51
49
  # @param [Array<String>] arguments
52
- # One or more command-line arguments
50
+ # A command and its arguments. The first element is the command name.
53
51
  # @param [Integer] timeout
54
52
  # The maximum allowed time for the command to run (in seconds)
55
53
  # @param [Integer] max_bytes
data/lib/llm/tools/git.rb CHANGED
@@ -16,8 +16,11 @@ class LLM::Tool
16
16
  defaults arguments: [], timeout: 5
17
17
 
18
18
  ##
19
- # @param [String] subcommand
20
- # @param [Array<String>, nil] arguments
19
+ # @param [Array<String>] arguments
20
+ # One or more git arguments. The first is the subcommand and must be
21
+ # one of `log`, `diff`, `commit`, `checkout`, `branch`, or `show`.
22
+ # @param [Integer] timeout
23
+ # The maximum time to allow the command to run (in seconds)
21
24
  # @return [Hash]
22
25
  def call(arguments: [], timeout: 5)
23
26
  subcommand = arguments[0]
@@ -4,7 +4,7 @@ class LLM::Tool
4
4
  ##
5
5
  # The {LLM::Tool::ReadFile} class implements a tool that
6
6
  # can read the contents of a file. It returns the content
7
- # as structured lines ({lineno:, content:}), so the model can
7
+ # as structured lines (`{lineno:, content:}`), so the model can
8
8
  # reference line numbers when requesting a narrower range.
9
9
  class ReadFile < self
10
10
  require_relative "utils"
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "15.2.0"
4
+ VERSION = "15.2.2"
5
5
  end
data/llm.gemspec CHANGED
@@ -37,13 +37,7 @@ DESCRIPTION
37
37
  spec.post_install_message = "\n" \
38
38
  "Got a question about llm.rb? " \
39
39
  "\n" \
40
- "Ask the https://r.uby.dev chatbot." \
41
- "\n" \
42
- "It is connected to the official GitHub repository." \
43
- "\n" \
44
- "100% free to use." \
45
- "\n" \
46
- "Built with llm.rb and DeepSeek." \
40
+ "The https://r.uby.dev website can help." \
47
41
  "\n\n"
48
42
 
49
43
  spec.add_development_dependency "webmock", "~> 3.24.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 15.2.0
4
+ version: 15.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Gleeson
@@ -695,9 +695,8 @@ metadata:
695
695
  source_code_uri: https://github.com/r-uby-dev/llm
696
696
  documentation_uri: https://r.uby.dev
697
697
  changelog_uri: https://github.com/r-uby-dev/llm/blob/main/CHANGELOG.md
698
- post_install_message: "\nGot a question about llm.rb? \nAsk the https://r.uby.dev
699
- chatbot.\nIt is connected to the official GitHub repository.\n100% free to use.\nBuilt
700
- with llm.rb and DeepSeek.\n\n"
698
+ post_install_message: "\nGot a question about llm.rb? \nThe https://r.uby.dev website
699
+ can help.\n\n"
701
700
  rdoc_options: []
702
701
  require_paths:
703
702
  - lib