llm.rb 16.0.0.pre.7 → 16.0.0.pre.8
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 +14 -2
- data/docs/deepdive/advanced/cancellation.md +6 -3
- data/docs/deepdive/reference/tracer.md +9 -7
- data/lib/llm/error.rb +13 -2
- data/lib/llm/function/async/task.rb +28 -14
- data/lib/llm/function/fiber/task.rb +13 -2
- data/lib/llm/tracer.rb +6 -3
- data/lib/llm/version.rb +1 -1
- 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: d91d2b022e12ba720029c2083969db3438a9c449e733caa6a992a63baacde3b8
|
|
4
|
+
data.tar.gz: 498c7b90c52d7d642bfec0baa89ea27b86a5d9e6bf3e5eefb0abfebf8dc03d72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a5d53788c58de77e215369a3a6fd77485e8fd8fccc1107218fac2d5f6bbc417dd44ccba9e37344ffa9084ded1949c33c516f708f6f6d50aaf6a5baacf8fec2b
|
|
7
|
+
data.tar.gz: 3d29709c6a8fd38b3631464adb21c22a7bfa491f5a7e365f38b52a2f2a6c07da3f63443e27530de19c2ce5446ce35a778b1f200cfcef4b554e65ef8bf1ee35f7
|
data/CHANGELOG.md
CHANGED
|
@@ -29,6 +29,7 @@ Releases before v15 are kept in [changelog/old.md](changelog/old.md).
|
|
|
29
29
|
| a `set_tracer` method on an ORM model | `set tracer:` in the block, or `tracer:` on the wrapper |
|
|
30
30
|
| a `:thread` or `:fiber` cancel that arrives before the tool starts is dropped | the cancel is held and raised once the tool starts |
|
|
31
31
|
| a cancel that arrives between two requests does nothing | the turn ends there, and the caller sees `LLM::Interrupt` |
|
|
32
|
+
| a bare `rescue`, or `rescue => ex`, catches a cancel | `LLM::Interrupt` is outside `StandardError`, so both forms pass it through |
|
|
32
33
|
|
|
33
34
|
* **drop Ruby 3.3 support** <br>
|
|
34
35
|
The gem now requires Ruby 3.4 or later. Ruby 3.3 cannot hold an
|
|
@@ -57,8 +58,19 @@ Releases before v15 are kept in [changelog/old.md](changelog/old.md).
|
|
|
57
58
|
first. `LLM::Agent#run_loop` records that caller for as long as the turn
|
|
58
59
|
lasts, and it answers `interrupt!` through
|
|
59
60
|
[`LLM::Agent::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent/Interrupt.html).
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
The raise lands in the thread or fiber the turn runs on, so it unwinds out
|
|
62
|
+
of the turn to whoever called it.
|
|
63
|
+
|
|
64
|
+
* **interrupt: move `LLM::Interrupt` outside `StandardError`** <br>
|
|
65
|
+
`LLM::Interrupt` is now an `Exception` rather than an `LLM::Error`, so a
|
|
66
|
+
bare `rescue` and a `rescue => ex` pass a cancel through instead of
|
|
67
|
+
swallowing it. Before, a broad rescue in a tool, a tracer, or an
|
|
68
|
+
application could eat an interrupt, and the turn looked like one that had
|
|
69
|
+
ignored its cancel. It is not a `SignalException`, since an interrupt has
|
|
70
|
+
to be catchable and a signal is a framework's own condition. A caller that
|
|
71
|
+
handles a cancel names `LLM::Interrupt`; `#wait` and the tool's
|
|
72
|
+
`on_interrupt` hook are unchanged, and a task still carries the interrupt
|
|
73
|
+
back to the caller rather than raising it inside its reactor.
|
|
62
74
|
|
|
63
75
|
### ActiveRecord
|
|
64
76
|
|
|
@@ -93,9 +93,12 @@ A cancel is delivered at the point the turn has reached, which can be
|
|
|
93
93
|
any point of it: inside a stream callback, a compactor, a transformer,
|
|
94
94
|
a guard, or the persistence write a step makes.
|
|
95
95
|
[`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
sits outside `StandardError`, so neither a bare `rescue` nor a
|
|
97
|
+
`rescue => ex` catches it, and the language is what keeps a cancel from
|
|
98
|
+
being eaten rather than a convention every rescue has to remember. Code
|
|
99
|
+
that handles a cancel names `LLM::Interrupt`, the way the example above
|
|
100
|
+
does; it is not an `LLM::Error` either, so a rescue of the runtime's
|
|
101
|
+
error superclass does not catch it.
|
|
99
102
|
|
|
100
103
|
The runtime's own path out of a turn does not swallow an interrupt:
|
|
101
104
|
[`LLM::Context#try`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#try)
|
|
@@ -157,13 +157,15 @@ the tool runs and returns the span that `on_tool_finish` and
|
|
|
157
157
|
`on_tool_error` receive.
|
|
158
158
|
|
|
159
159
|
An interrupt is neither a finish nor an error, and it is reported to a
|
|
160
|
-
hook of its own. `on_interrupt` is called with a `scope:` of `:request
|
|
161
|
-
or `:
|
|
162
|
-
records what happened to a turn records it while the work is
|
|
163
|
-
flight. The hook receives the span that `on_request_start`
|
|
164
|
-
its `request_id`; a tool pass carries neither, because a
|
|
165
|
-
belongs to the call
|
|
166
|
-
|
|
160
|
+
hook of its own. `on_interrupt` is called with a `scope:` of `:request`,
|
|
161
|
+
`:tool`, or `:agent` before the caller is given the exception, so a
|
|
162
|
+
tracer that records what happened to a turn records it while the work is
|
|
163
|
+
still in flight. The hook receives the span that `on_request_start`
|
|
164
|
+
returned and its `request_id`; a tool pass carries neither, because a
|
|
165
|
+
tool's span belongs to the call, and an `:agent` pass is one that landed
|
|
166
|
+
between a turn's requests, where there was nothing more precise to
|
|
167
|
+
interrupt. A cancel reaches every tool that is running, so the tool phase
|
|
168
|
+
is announced once rather than once per tool.
|
|
167
169
|
|
|
168
170
|
A tracer's own lifetime is bracketed as well. `on_exit` fires once,
|
|
169
171
|
when the last scope that is open for that tracer ends. That scope can
|
data/lib/llm/error.rb
CHANGED
|
@@ -63,8 +63,19 @@ module LLM
|
|
|
63
63
|
ContextWindowError = Class.new(InvalidRequestError)
|
|
64
64
|
|
|
65
65
|
##
|
|
66
|
-
# When a request is interrupted
|
|
67
|
-
|
|
66
|
+
# When a request is interrupted.
|
|
67
|
+
#
|
|
68
|
+
# It sits outside `StandardError`, so that neither a bare `rescue` nor a
|
|
69
|
+
# `rescue => e` can swallow it by mistake: an interrupt is a request to
|
|
70
|
+
# stop, and a turn whose cancel was eaten looks like a turn that ignored
|
|
71
|
+
# one.
|
|
72
|
+
#
|
|
73
|
+
# It is not a `SignalException` either. A signal is a framework's own
|
|
74
|
+
# condition rather than a task's - RSpec re-raises one that escapes an
|
|
75
|
+
# example, and an async reactor ends its own thread rather than failing
|
|
76
|
+
# the task that raised it - and an interrupt has to be something a caller
|
|
77
|
+
# can catch.
|
|
78
|
+
Interrupt = Class.new(Exception)
|
|
68
79
|
|
|
69
80
|
##
|
|
70
81
|
# When a concurrency strategy cannot execute a given tool
|
|
@@ -49,13 +49,15 @@ module LLM::Function::Async
|
|
|
49
49
|
# hook that runs after the queue was pushed is a hook the caller can
|
|
50
50
|
# race past.
|
|
51
51
|
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
52
|
+
# `LLM::Interrupt` is a subclass of `Exception`, and one that is left to
|
|
53
|
+
# raise kills the reactor's thread and takes every other task on that
|
|
54
|
+
# reactor with it. So the task rescues it, stores it on its own queue,
|
|
55
|
+
# and exits silently - and the caller reads the queue and raises the
|
|
56
|
+
# interrupt on its own thread or fiber, which is where it was asked for.
|
|
57
|
+
#
|
|
58
|
+
# The rescue below names the interrupt rather than leaving it to a bare
|
|
59
|
+
# rescue, because a bare rescue - and `rescue => e` - reaches only
|
|
60
|
+
# `StandardError`.
|
|
59
61
|
# @return [nil]
|
|
60
62
|
def spawn
|
|
61
63
|
return if @guarded
|
|
@@ -65,10 +67,22 @@ module LLM::Function::Async
|
|
|
65
67
|
task = Async::Task.current
|
|
66
68
|
@task = task
|
|
67
69
|
@scheduler = Fiber.scheduler
|
|
68
|
-
|
|
70
|
+
##
|
|
71
|
+
# A cancel that arrived before this ran. There is no tool to tell,
|
|
72
|
+
# so the caller is given it and the task ends - for the same reason
|
|
73
|
+
# the block below answers rather than raises.
|
|
74
|
+
if @cancelled
|
|
75
|
+
@queue << LLM::Interrupt.new
|
|
76
|
+
next
|
|
77
|
+
end
|
|
69
78
|
task.defer_cancel do
|
|
70
79
|
result = begin
|
|
71
80
|
function.call
|
|
81
|
+
rescue LLM::Interrupt => ex
|
|
82
|
+
##
|
|
83
|
+
# The interrupt is the block's result, and the queue carries it
|
|
84
|
+
# to the caller.
|
|
85
|
+
ex
|
|
72
86
|
ensure
|
|
73
87
|
##
|
|
74
88
|
# The hook runs on the reactor's thread rather than on the one
|
|
@@ -80,15 +94,15 @@ module LLM::Function::Async
|
|
|
80
94
|
end
|
|
81
95
|
@queue << result
|
|
82
96
|
end
|
|
83
|
-
rescue => e
|
|
97
|
+
rescue LLM::Interrupt, StandardError => e
|
|
84
98
|
##
|
|
85
99
|
# This runs after the `defer_cancel` block's `ensure`, so the tool is
|
|
86
|
-
# told first and the caller second. It answers
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
100
|
+
# told first and the caller second. It answers a hook whose own
|
|
101
|
+
# error unwound past the push, and it is the backstop for an
|
|
102
|
+
# interrupt that arrived from somewhere other than the call above -
|
|
103
|
+
# which is why the interrupt is pushed and not raised on.
|
|
90
104
|
@queue << e
|
|
91
|
-
raise
|
|
105
|
+
raise unless LLM::Interrupt === e
|
|
92
106
|
end
|
|
93
107
|
nil
|
|
94
108
|
end
|
|
@@ -72,9 +72,20 @@ module LLM::Function::Fiber
|
|
|
72
72
|
raise LLM::Interrupt
|
|
73
73
|
end
|
|
74
74
|
@queue << function.call
|
|
75
|
-
rescue => ex
|
|
75
|
+
rescue LLM::Interrupt, StandardError => ex
|
|
76
|
+
##
|
|
77
|
+
# The interrupt is stored on the queue rather than raised on.
|
|
78
|
+
# `LLM::Interrupt` is a subclass of `Exception`, and one that is
|
|
79
|
+
# left to raise kills the scheduler's thread and takes the tasks
|
|
80
|
+
# running on it with it - the caller reads the queue and raises
|
|
81
|
+
# the interrupt on its own thread or fiber, and this task exits
|
|
82
|
+
# silently.
|
|
83
|
+
#
|
|
84
|
+
# The rescue names the interrupt rather than leaving it to a bare
|
|
85
|
+
# rescue, because a bare rescue - and `rescue => ex` - reaches
|
|
86
|
+
# only `StandardError`.
|
|
76
87
|
@queue << ex
|
|
77
|
-
raise
|
|
88
|
+
raise unless LLM::Interrupt === ex
|
|
78
89
|
ensure
|
|
79
90
|
##
|
|
80
91
|
# The hook runs on the fiber the call runs on, once the call has
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -195,7 +195,7 @@ module LLM
|
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
##
|
|
198
|
-
# Called when a request or a
|
|
198
|
+
# Called when a request, a tool, or a turn is interrupted.
|
|
199
199
|
#
|
|
200
200
|
# An interrupt is not a failure, so this is not {#on_request_error} or
|
|
201
201
|
# {#on_tool_error} under another name: it is the third ending either
|
|
@@ -206,7 +206,9 @@ module LLM
|
|
|
206
206
|
# A request is announced once, by the transport. A tool is announced
|
|
207
207
|
# once for the phase rather than once per tool: a cancel reaches every
|
|
208
208
|
# tool that is running and the caller hears one exception, so there is
|
|
209
|
-
# no one tool the announcement belongs to.
|
|
209
|
+
# no one tool the announcement belongs to. A turn is announced as
|
|
210
|
+
# `:agent` when the interrupt lands between two of its requests, which
|
|
211
|
+
# is the one point of a turn that has no span of its own.
|
|
210
212
|
#
|
|
211
213
|
# The default does nothing, which is the difference from the rest of the
|
|
212
214
|
# lifecycle. Every other hook raises, because a tracer that means to draw
|
|
@@ -214,7 +216,8 @@ module LLM
|
|
|
214
216
|
# whatever tracer happens to be bound, and a hook that raised here would
|
|
215
217
|
# replace the interrupt every caller is written against.
|
|
216
218
|
# @param [Symbol] scope
|
|
217
|
-
# :request for a request, :tool for a tool
|
|
219
|
+
# :request for a request, :tool for a tool, :agent for a turn that is
|
|
220
|
+
# between its requests
|
|
218
221
|
# @param [Object, nil] span
|
|
219
222
|
# The span that {#on_request_start} returned. A tool pass has none:
|
|
220
223
|
# a tool's span belongs to the call, and the announcement is not one
|
data/lib/llm/version.rb
CHANGED