llm.rb 12.5.1 → 12.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/README.md +4 -5
- data/lib/llm/active_record/acts_as_agent.rb +32 -0
- data/lib/llm/agent.rb +40 -4
- data/lib/llm/context.rb +2 -0
- data/lib/llm/function/call_group.rb +5 -0
- data/lib/llm/function/fiber_group.rb +1 -1
- data/lib/llm/function/fork/job.rb +3 -2
- data/lib/llm/function/fork/task.rb +1 -0
- data/lib/llm/function/ractor/job.rb +13 -3
- data/lib/llm/function/ractor/mailbox.rb +7 -0
- data/lib/llm/function/ractor/task.rb +1 -0
- data/lib/llm/function/task.rb +12 -1
- data/lib/llm/function/thread_group.rb +1 -1
- data/lib/llm/function/tracing.rb +2 -0
- data/lib/llm/function.rb +3 -1
- data/lib/llm/sequel/agent.rb +32 -0
- data/lib/llm/tool/param.rb +12 -0
- data/lib/llm/version.rb +1 -1
- data/resources/deepdive.md +54 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1660ea34a112d4c31eb8c9a51000ecd244b0f5629a4f763eceecd734d96f765b
|
|
4
|
+
data.tar.gz: 8040eefe13ffc48a40181642dad749d6abd7d16950f5d9e379ef006a0bb5af0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc1821c04eb0f996ca3e5d86d1722565e1fe2bc13351f3771c57de556d7ee9e97d930b3516ec70dea9686fad6443498c40d50584c6065a95242318f054a98d83
|
|
7
|
+
data.tar.gz: 3cc468ef1332c6f39d6b34a6a11fafe91eaf459ed80d5e79709f8106278fcb6fa1ba4f67b701a38539c3cba4d6967ad88858cccb4bb6b5b12690290036ad2afb
|
data/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,101 @@
|
|
|
15
15
|
|
|
16
16
|
## What's next
|
|
17
17
|
|
|
18
|
+
## v12.6.0
|
|
19
|
+
|
|
20
|
+
Changes since `v12.5.1`.
|
|
21
|
+
|
|
22
|
+
This release adds bulk defaults for tools and agents — `LLM::Tool.defaults`
|
|
23
|
+
for setting parameter defaults and `LLM::Agent.set` for mass-assigning
|
|
24
|
+
class-level defaults, both mirrored on ActiveRecord and Sequel agent models.
|
|
25
|
+
|
|
26
|
+
It also makes `LLM::Interrupt` reliable across every concurrency strategy
|
|
27
|
+
(:thread, :call, :fiber, :task, :fork, and :ractor) so tool cancellation
|
|
28
|
+
works consistently regardless of execution backend, and fixes a stale fiber
|
|
29
|
+
reference in `LLM::Context#talk` that could prevent interruption after a
|
|
30
|
+
prior call.
|
|
31
|
+
|
|
32
|
+
### Add
|
|
33
|
+
|
|
34
|
+
* **tool: add `defaults` method for setting parameter defaults** <br>
|
|
35
|
+
Add `LLM::Tool.defaults(properties)` for bulk-setting default values
|
|
36
|
+
on tool parameters, matching the same interface as `LLM::Schema.defaults`.
|
|
37
|
+
Each key maps to a parameter name; unknown keys raise `KeyError`.
|
|
38
|
+
|
|
39
|
+
* **agent: add `set` method for bulk-assigning class-level defaults** <br>
|
|
40
|
+
Add `LLM::Agent.set(properties)` for mass-assigning agent defaults
|
|
41
|
+
from a Hash. Each key maps to a class-level accessor; unknown keys
|
|
42
|
+
raise `KeyError`.
|
|
43
|
+
|
|
44
|
+
* **active_record: expose `set` on `acts_as_agent` models** <br>
|
|
45
|
+
ActiveRecord models using `acts_as_agent` can call `set` to
|
|
46
|
+
bulk-assign agent class-level defaults.
|
|
47
|
+
|
|
48
|
+
* **sequel: expose `set` on `plugin :agent` models** <br>
|
|
49
|
+
Sequel models using `plugin :agent` can call `set` to bulk-assign
|
|
50
|
+
agent class-level defaults.
|
|
51
|
+
|
|
52
|
+
### Fix
|
|
53
|
+
|
|
54
|
+
* **function: raise `LLM::Interrupt` on thread where tool is running** <br>
|
|
55
|
+
On cancel, `LLM::Interrupt` is now raised on the thread that is
|
|
56
|
+
running a tool. The tool can rescue `LLM::Interrupt` and gracefully
|
|
57
|
+
terminate (e.g., clean up resources). The previous approach used
|
|
58
|
+
`Thread#interrupt` which was less reliable — it did not interrupt a
|
|
59
|
+
sleeping thread.
|
|
60
|
+
|
|
61
|
+
* **function: suppress thread exception reporting in `:thread` concurrency** <br>
|
|
62
|
+
Threads spawned by the `:thread` concurrency strategy now have
|
|
63
|
+
`report_on_exception` set to `false`, preventing noisy exception
|
|
64
|
+
messages from appearing on stderr when a thread is interrupted
|
|
65
|
+
during tool execution.
|
|
66
|
+
|
|
67
|
+
* **context: clear `@owner` after `talk` completes** <br>
|
|
68
|
+
`LLM::Context#talk` now clears the `@owner` reference in an
|
|
69
|
+
`ensure` block after the method completes, so `interrupt!` does
|
|
70
|
+
not attempt to interrupt a stale fiber reference from a prior
|
|
71
|
+
call.
|
|
72
|
+
|
|
73
|
+
* **function: raise `LLM::Interrupt` on thread waiting in `CallGroup#wait`** <br>
|
|
74
|
+
When using `ctx.wait(:call)`, `LLM::Interrupt` is now raised on the
|
|
75
|
+
thread executing the sequential tool wait. `CallGroup#wait` tracks the
|
|
76
|
+
active thread and `interrupt!` raises `LLM::Interrupt` on it, enabling
|
|
77
|
+
interruption of the `:call` concurrency strategy just like the existing
|
|
78
|
+
`:thread` strategy.
|
|
79
|
+
|
|
80
|
+
* **function: raise `LLM::Interrupt` on fiber-backed tool tasks** <br>
|
|
81
|
+
`LLM::Interrupt` is now raised on the active fiber via `Fiber#raise`
|
|
82
|
+
when interrupting `:fiber`-concurrency tools.
|
|
83
|
+
<br><br>
|
|
84
|
+
`Task#interrupt!` now dispatches by task type — `Thread#raise` for
|
|
85
|
+
threads, `Fiber#raise` for fibers — making interruption reliable
|
|
86
|
+
across all concurrency strategies.
|
|
87
|
+
|
|
88
|
+
* **function: raise `LLM::Interrupt` on fork-backed tool tasks** <br>
|
|
89
|
+
`LLM::Interrupt` is now raised on the main thread of a fork child
|
|
90
|
+
process via `Thread.main.raise(LLM::Interrupt)` when interrupting
|
|
91
|
+
`:fork`-concurrency tools, and the fork `Task#wait` re-raises the
|
|
92
|
+
interrupt on the parent side — making interruption reliable across
|
|
93
|
+
all concurrency strategies including `:fork`.
|
|
94
|
+
|
|
95
|
+
* **function: raise `LLM::Interrupt` on `Async::Task`-backed tool tasks** <br>
|
|
96
|
+
`LLM::Interrupt` is now raised on the underlying fiber of an
|
|
97
|
+
`Async::Task` via `Fiber#raise` when interrupting `:task`-concurrency
|
|
98
|
+
tools. `Task#interrupt!` now detects `Async::Task` instances and
|
|
99
|
+
dispatches to `Fiber#raise`, extending reliable interruption to the
|
|
100
|
+
`:task` concurrency strategy under the Async runtime.
|
|
101
|
+
|
|
102
|
+
* **function: raise `LLM::Interrupt` on ractor-backed tool tasks** <br>
|
|
103
|
+
`LLM::Interrupt` is now raised on the main thread inside a ractor
|
|
104
|
+
via `Thread.main.raise(LLM::Interrupt)` when interrupting
|
|
105
|
+
`:ractor`-concurrency tools. A listener thread inside the tool
|
|
106
|
+
ractor waits for an interrupt message via `Ractor.receive` and
|
|
107
|
+
raises `LLM::Interrupt` on the ractor's main thread.
|
|
108
|
+
<br><br>
|
|
109
|
+
`Task#interrupt!` delegates to the mailbox to send the interrupt
|
|
110
|
+
message — extending reliable interruption to the `:ractor`
|
|
111
|
+
concurrency strategy.
|
|
112
|
+
|
|
18
113
|
## v12.5.1
|
|
19
114
|
|
|
20
115
|
Changes since `v12.5.0`.
|
data/README.md
CHANGED
|
@@ -362,11 +362,10 @@ require "llm"
|
|
|
362
362
|
require "llm/active_record"
|
|
363
363
|
|
|
364
364
|
class Agent < ApplicationRecord
|
|
365
|
-
acts_as_agent
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
end
|
|
365
|
+
acts_as_agent
|
|
366
|
+
set instructions: "solve the user's query",
|
|
367
|
+
model: "deepseek-v4-pro",
|
|
368
|
+
tools: [Research, FinalizeResearch, ActOnResearch]
|
|
370
369
|
|
|
371
370
|
private
|
|
372
371
|
|
|
@@ -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
|
data/lib/llm/agent.rb
CHANGED
|
@@ -27,10 +27,10 @@ module LLM
|
|
|
27
27
|
#
|
|
28
28
|
# @example
|
|
29
29
|
# class SystemAdmin < LLM::Agent
|
|
30
|
-
# model "gpt-4.1-nano"
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
30
|
+
# set model: "gpt-4.1-nano",
|
|
31
|
+
# instructions: "You are a Linux system admin",
|
|
32
|
+
# tools: [Shell],
|
|
33
|
+
# schema: Result
|
|
34
34
|
# end
|
|
35
35
|
#
|
|
36
36
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
@@ -42,6 +42,42 @@ module LLM
|
|
|
42
42
|
# @return [LLM::Provider]
|
|
43
43
|
attr_reader :llm
|
|
44
44
|
|
|
45
|
+
##
|
|
46
|
+
# Bulk-assign class-level agent defaults from a Hash.
|
|
47
|
+
#
|
|
48
|
+
# Each key is resolved by calling the corresponding class method on the
|
|
49
|
+
# agent subclass. An error is raised for unknown keys so that typos are
|
|
50
|
+
# caught early.
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# class AdminAgent < LLM::Agent
|
|
54
|
+
# set instructions: "You are a system administrator",
|
|
55
|
+
# model: "gpt-4.1-nano",
|
|
56
|
+
# tools: [Shell, ReadFile]
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# @param [Hash] properties
|
|
60
|
+
# @option properties [String] :instructions
|
|
61
|
+
# @option properties [String] :model
|
|
62
|
+
# @option properties [Array<LLM::Function>] :tools
|
|
63
|
+
# @option properties [Array<String>] :skills
|
|
64
|
+
# @option properties [#to_json] :schema
|
|
65
|
+
# @option properties [Symbol, Array<Symbol>] :concurrency
|
|
66
|
+
# @option properties [LLM::Tracer, Proc] :tracer
|
|
67
|
+
# @option properties [Object, Proc] :stream
|
|
68
|
+
# @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
|
|
69
|
+
# @raise [KeyError] when a property key does not match a class-level accessor
|
|
70
|
+
# @return [void]
|
|
71
|
+
def self.set(properties)
|
|
72
|
+
properties.each do
|
|
73
|
+
if respond_to?(_1)
|
|
74
|
+
public_send(_1, _2)
|
|
75
|
+
else
|
|
76
|
+
raise KeyError, "key not found: #{_1}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
45
81
|
##
|
|
46
82
|
# Set or get the default model
|
|
47
83
|
# @param [String, nil] model
|
data/lib/llm/context.rb
CHANGED
|
@@ -14,6 +14,7 @@ class LLM::Function
|
|
|
14
14
|
# @return [LLM::Function::CallGroup]
|
|
15
15
|
def initialize(functions)
|
|
16
16
|
@functions = functions
|
|
17
|
+
@owner = nil
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
##
|
|
@@ -25,6 +26,7 @@ class LLM::Function
|
|
|
25
26
|
##
|
|
26
27
|
# @return [nil]
|
|
27
28
|
def interrupt!
|
|
29
|
+
@owner&.raise(LLM::Interrupt)
|
|
28
30
|
nil
|
|
29
31
|
end
|
|
30
32
|
alias_method :cancel!, :interrupt!
|
|
@@ -32,7 +34,10 @@ class LLM::Function
|
|
|
32
34
|
##
|
|
33
35
|
# @return [Array<LLM::Function::Return>]
|
|
34
36
|
def wait
|
|
37
|
+
@owner = Thread.current
|
|
35
38
|
@functions.map(&:call)
|
|
39
|
+
ensure
|
|
40
|
+
@owner = nil
|
|
36
41
|
end
|
|
37
42
|
alias_method :value, :wait
|
|
38
43
|
end
|
|
@@ -24,6 +24,8 @@ class LLM::Function
|
|
|
24
24
|
runner = @function.runner
|
|
25
25
|
controller = setup(runner)
|
|
26
26
|
@ch.result.write([:result, call!(runner)])
|
|
27
|
+
rescue LLM::Interrupt
|
|
28
|
+
@ch.result.write([:interrupt])
|
|
27
29
|
rescue => ex
|
|
28
30
|
@ch.result.write([:result, error(ex)])
|
|
29
31
|
ensure
|
|
@@ -56,8 +58,7 @@ class LLM::Function
|
|
|
56
58
|
ready << true
|
|
57
59
|
kind = @ch.control.recv
|
|
58
60
|
next unless kind == :interrupt
|
|
59
|
-
|
|
60
|
-
runner.public_send(hook) if hook
|
|
61
|
+
Thread.main.raise(LLM::Interrupt)
|
|
61
62
|
rescue IOError, ArgumentError
|
|
62
63
|
end
|
|
63
64
|
ready.pop
|
|
@@ -52,6 +52,7 @@ class LLM::Function
|
|
|
52
52
|
# @return [LLM::Function::Return]
|
|
53
53
|
def wait
|
|
54
54
|
kind, data = @ch.result.recv
|
|
55
|
+
raise LLM::Interrupt if kind == :interrupt
|
|
55
56
|
raise ArgumentError, "Unknown fork message: #{kind.inspect}" unless kind == :result
|
|
56
57
|
result = Return.new(data[:id], data[:name], data[:value])
|
|
57
58
|
reap
|
|
@@ -35,22 +35,32 @@ class LLM::Function
|
|
|
35
35
|
waiters = []
|
|
36
36
|
loop do
|
|
37
37
|
case ::Ractor.receive
|
|
38
|
-
in [:done, *
|
|
38
|
+
in [:done, *data]
|
|
39
|
+
result ||= data
|
|
39
40
|
done = true
|
|
40
41
|
waiters.each { _1.send(result) }
|
|
41
42
|
waiters.clear
|
|
42
43
|
in [:alive?, reply]
|
|
43
44
|
reply.send(!done)
|
|
44
45
|
in [:wait, reply]
|
|
45
|
-
done ? reply.send(result) : waiters << reply
|
|
46
|
+
done ? reply.send(result) : (waiters << reply)
|
|
47
|
+
in [:interrupt]
|
|
48
|
+
@tool&.send(:interrupt)
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def spawn
|
|
51
|
-
::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
|
|
54
|
+
@tool = ::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
|
|
55
|
+
Thread.new do
|
|
56
|
+
::Ractor.receive == :interrupt or next
|
|
57
|
+
Thread.main.raise(LLM::Interrupt)
|
|
58
|
+
rescue ::Ractor::Error
|
|
59
|
+
end
|
|
52
60
|
kwargs = Hash === arguments ? arguments.transform_keys(&:to_sym) : arguments
|
|
53
61
|
mailbox.send([:done, id, name, runner_class.new.call(**kwargs)])
|
|
62
|
+
rescue LLM::Interrupt
|
|
63
|
+
mailbox.send([:done, id, name, {cancelled: true, reason: "interrupted"}])
|
|
54
64
|
rescue => ex
|
|
55
65
|
mailbox.send([:done, id, name, {error: true, type: ex.class.name, message: ex.message}])
|
|
56
66
|
end
|
data/lib/llm/function/task.rb
CHANGED
|
@@ -33,7 +33,18 @@ class LLM::Function
|
|
|
33
33
|
##
|
|
34
34
|
# @return [nil]
|
|
35
35
|
def interrupt!
|
|
36
|
-
|
|
36
|
+
case task
|
|
37
|
+
when Thread
|
|
38
|
+
task.raise(LLM::Interrupt)
|
|
39
|
+
when Fiber
|
|
40
|
+
task.raise(LLM::Interrupt) if task.alive?
|
|
41
|
+
else
|
|
42
|
+
if defined?(::Async::Task) and ::Async::Task === task
|
|
43
|
+
task.fiber&.raise(LLM::Interrupt) if task.alive?
|
|
44
|
+
elsif task.respond_to?(:interrupt!)
|
|
45
|
+
task.interrupt!
|
|
46
|
+
end
|
|
47
|
+
end
|
|
37
48
|
function&.interrupt!
|
|
38
49
|
nil
|
|
39
50
|
end
|
data/lib/llm/function/tracing.rb
CHANGED
data/lib/llm/function.rb
CHANGED
|
@@ -256,7 +256,7 @@ class LLM::Function
|
|
|
256
256
|
LLM.require "async" unless defined?(::Async)
|
|
257
257
|
Async { call! }
|
|
258
258
|
when :thread
|
|
259
|
-
Thread.new { call! }
|
|
259
|
+
Thread.new { call! }.tap { _1.report_on_exception = false }
|
|
260
260
|
when :fiber
|
|
261
261
|
raise ArgumentError, "Fiber concurrency requires Fiber.scheduler" unless Fiber.scheduler
|
|
262
262
|
Fiber.schedule { call! }
|
|
@@ -382,6 +382,8 @@ class LLM::Function
|
|
|
382
382
|
runner = self.runner
|
|
383
383
|
kwargs = arguments.respond_to?(:to_h) ? arguments.to_h.transform_keys(&:to_sym) : arguments
|
|
384
384
|
Return.new(id, name, runner.call(**kwargs))
|
|
385
|
+
rescue LLM::Interrupt
|
|
386
|
+
raise
|
|
385
387
|
rescue => ex
|
|
386
388
|
Return.new(id, name, {error: true, type: ex.class.name, message: ex.message})
|
|
387
389
|
end
|
data/lib/llm/sequel/agent.rb
CHANGED
|
@@ -32,9 +32,41 @@ module LLM::Sequel
|
|
|
32
32
|
@llm_agent_options || Agent::DEFAULTS
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
##
|
|
36
|
+
# @return [Class<LLM::Agent>]
|
|
35
37
|
def agent
|
|
36
38
|
@agent ||= Class.new(LLM::Agent)
|
|
37
39
|
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Bulk-assign class-level agent defaults.
|
|
43
|
+
#
|
|
44
|
+
# Each key is resolved by calling the corresponding class method on the
|
|
45
|
+
# internal agent subclass.
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# class Agent < Sequel::Model
|
|
49
|
+
# plugin :llm_agent
|
|
50
|
+
# set instructions: "You are a system administrator",
|
|
51
|
+
# model: "gpt-4.1-nano",
|
|
52
|
+
# tools: [Shell]
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# @param [Hash] properties
|
|
56
|
+
# @option properties [String] :instructions
|
|
57
|
+
# @option properties [String] :model
|
|
58
|
+
# @option properties [Array<LLM::Function>] :tools
|
|
59
|
+
# @option properties [Array<String>] :skills
|
|
60
|
+
# @option properties [#to_json] :schema
|
|
61
|
+
# @option properties [Symbol, Array<Symbol>] :concurrency
|
|
62
|
+
# @option properties [LLM::Tracer, Proc] :tracer
|
|
63
|
+
# @option properties [Object, Proc] :stream
|
|
64
|
+
# @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
|
|
65
|
+
# @raise [KeyError] when a property key does not match a class-level accessor
|
|
66
|
+
# @return [void]
|
|
67
|
+
def set(properties)
|
|
68
|
+
agent.set(properties)
|
|
69
|
+
end
|
|
38
70
|
end
|
|
39
71
|
|
|
40
72
|
module InstanceMethods
|
data/lib/llm/tool/param.rb
CHANGED
|
@@ -56,6 +56,18 @@ class LLM::Tool
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
##
|
|
60
|
+
# Set default values for parameters.
|
|
61
|
+
# @param [Hash] defaults
|
|
62
|
+
# @return [LLM::Schema::Object]
|
|
63
|
+
def defaults(defaults)
|
|
64
|
+
lock do
|
|
65
|
+
function.params.tap do |schema|
|
|
66
|
+
defaults.each { Utils.fetch(schema.properties, _1).default(_2) }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
59
71
|
##
|
|
60
72
|
# @api private
|
|
61
73
|
module Utils
|
data/lib/llm/version.rb
CHANGED
data/resources/deepdive.md
CHANGED
|
@@ -71,6 +71,7 @@ features that didn't make it into the homepage documentation.
|
|
|
71
71
|
<summary>Cancellation</summary>
|
|
72
72
|
|
|
73
73
|
- [Cancel a request](#cancel-a-request)
|
|
74
|
+
- [Tool interrupts](#tool-interrupts)
|
|
74
75
|
</details>
|
|
75
76
|
|
|
76
77
|
<details>
|
|
@@ -188,11 +189,11 @@ be simpler without it.
|
|
|
188
189
|
|
|
189
190
|
```ruby
|
|
190
191
|
class Agent < LLM::Agent
|
|
191
|
-
model "deepseek-v4-pro"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
set model: "deepseek-v4-pro",
|
|
193
|
+
tools: [DoResearch, FinalizeResearch, ActOnResearch],
|
|
194
|
+
stream: -> { $stdout },
|
|
195
|
+
tracer: :set_tracer,
|
|
196
|
+
concurrency: :fork
|
|
196
197
|
|
|
197
198
|
def research!
|
|
198
199
|
talk "start the research"
|
|
@@ -828,6 +829,54 @@ rescue LLM::Interrupt
|
|
|
828
829
|
end
|
|
829
830
|
```
|
|
830
831
|
|
|
832
|
+
#### Tool interrupts
|
|
833
|
+
|
|
834
|
+
When a running tool is interrupted – for example the user presses
|
|
835
|
+
ESC in the [REPL](#repl) – the runtime raises
|
|
836
|
+
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
837
|
+
on the tool's execution context. This behavior is uniform across
|
|
838
|
+
all six concurrency strategies (`:call`, `:thread`, `:fiber`,
|
|
839
|
+
`:task`, `:fork`, and `:ractor`).
|
|
840
|
+
|
|
841
|
+
A tool has two choices:
|
|
842
|
+
|
|
843
|
+
**Re-raise** `LLM::Interrupt` to cancel the entire turn. The
|
|
844
|
+
exception propagates out of the tool loop and the request is
|
|
845
|
+
aborted. This is the default when you don't rescue the exception.
|
|
846
|
+
|
|
847
|
+
```ruby
|
|
848
|
+
def call
|
|
849
|
+
# ... do work ...
|
|
850
|
+
rescue LLM::Interrupt
|
|
851
|
+
cleanup
|
|
852
|
+
raise # cancel the turn
|
|
853
|
+
end
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
**Return a value** to continue the tool loop. The model receives
|
|
857
|
+
the result and decides what to do next, aware that the tool was
|
|
858
|
+
interrupted.
|
|
859
|
+
|
|
860
|
+
```ruby
|
|
861
|
+
def call
|
|
862
|
+
# ... do work ...
|
|
863
|
+
rescue LLM::Interrupt
|
|
864
|
+
cleanup
|
|
865
|
+
{ok: false, reason: "interrupted"} # continue the loop
|
|
866
|
+
end
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
The right choice depends on the situation. A hard cancel aborts
|
|
870
|
+
the request outright – useful when continuing would produce
|
|
871
|
+
garbage. Returning a value lets the model adapt, which can be
|
|
872
|
+
helpful when the interrupt is temporary (e.g. a timeout).
|
|
873
|
+
|
|
874
|
+
The `:ractor` strategy is cooperative by nature – the ractor
|
|
875
|
+
must check for a cancel message at safe points in its execution.
|
|
876
|
+
The `:fork` strategy delivers the interrupt via a signal, which
|
|
877
|
+
interrupts blocking syscalls immediately. All other strategies
|
|
878
|
+
raise the exception directly on the executing thread or fiber.
|
|
879
|
+
|
|
831
880
|
[Back to top](#table-of-contents)
|
|
832
881
|
|
|
833
882
|
## Tracer
|