llm.rb 15.2.0 → 15.2.1

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: 8437f48c9a995054696648dfbe3386a5f0d8c8a62379765d2eca8694cf3adb3b
4
+ data.tar.gz: d2c36a69342e2fef94472abc25e813a0ee8bbef9aba15e68b275d89d307a6405
5
5
  SHA512:
6
- metadata.gz: 8e0d5dd6c98355412a31dcb638081ea9f0553725cbf30aa6598eed5d16cd6d4693b5edfae8c063625ee2f5198b342e9b11850897370404267d291ddae4d747fe
7
- data.tar.gz: 25102554a840fee5046804d17ff91673793603b96ab9ae246a30b7d3192a792e9bd7e384d76ce20abf2d504910330202cb2d4aef0057c7875c1ce3866ccfe177
6
+ metadata.gz: d1d120fe448ee693ba627cfdfe213cd42030fe8448f72c8cf6ecaddb62d2ac5f7b07ded6191142ec28571b41acbdf8c96b458420889e7fb75990f53f51b3ab43
7
+ data.tar.gz: 816b6feffe0339f52798062de3f36188b3d6574203289f450fd07de2e1e600b29aa956a8ba50947d3ae27540a4132222f3b31bb865784f88e24074388a852058
data/CHANGELOG.md CHANGED
@@ -17,6 +17,46 @@
17
17
 
18
18
  *No unreleased changes yet. Check back after the next release.*
19
19
 
20
+ ## v15.2.1
21
+
22
+ Changes since `v15.2.0`.
23
+
24
+ This release makes the agent count its tool budget across the whole turn
25
+ and emit tool returns to the stream once that budget is spent, and lets
26
+ `LLM::Function#cancel` carry extra return fields.
27
+
28
+ ### Agent
29
+
30
+ * **agent: count the tool budget across the whole turn** <br>
31
+ [`LLM::Agent.tool_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#tool_budget-class_method)
32
+ now caps the tool calls a turn runs in total. A batch of calls is
33
+ spent as a batch, and a batch that would take the turn past its
34
+ budget is not run at all; once the budget is spent no tool call runs,
35
+ and the agent keeps sending its in-band advisory until the model
36
+ answers without requesting more tools. Previously the agent ran up to
37
+ the budget, then ran further batches after each advisory, so a turn
38
+ could run more tool calls than its budget allowed.
39
+
40
+ * **agent: emit tool returns when the budget is spent** <br>
41
+ Fix a bug where, once a turn's tool budget was spent, the agent passed
42
+ its in-band returns to the model without emitting them to the stream.
43
+ The stream went silent, so a stream that tracked tool-call state left
44
+ the calls in the `call` state. The returns are now emitted through
45
+ [`LLM::Stream#on_tool_return`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_tool_return-instance_method)
46
+ like any other tool return.
47
+
48
+ ### Function
49
+
50
+ * **function: `LLM::Function#cancel` takes extra return fields** <br>
51
+ [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method)
52
+ now accepts keywords beyond `reason:` and merges them into the
53
+ [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html)
54
+ it builds. `LLM::Function#budget_spent` now builds its return through
55
+ `cancel` instead of hand-rolling an error, so a budget-spent return is
56
+ marked `cancelled: true` and carries `action:` and `advice:` hints. A
57
+ callback that receives returns can now tell a cancellation apart from
58
+ an ordinary error.
59
+
20
60
  ## v15.2.0
21
61
 
22
62
  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
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
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
  ##
@@ -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
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.1"
5
5
  end
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Gleeson