llm.rb 16.0.0 → 17.0.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 +4 -4
- data/CHANGELOG.md +535 -2
- data/README.md +857 -501
- data/data/alibaba.json +2 -2
- data/data/anthropic.json +66 -3
- data/data/bedrock.json +781 -12
- data/data/deepinfra.json +157 -1
- data/data/google.json +48 -0
- data/data/mistral.json +326 -16
- data/data/moonshot.json +2 -1
- data/data/openai.json +13 -0
- data/data/openrouter.json +1078 -1235
- data/data/xai.json +4 -2
- data/docs/deepdive/advanced/cancellation.md +35 -7
- data/docs/deepdive/advanced/compaction.md +5 -5
- data/docs/deepdive/advanced/context.md +45 -24
- data/docs/deepdive/advanced/{guard.md → intercept.md} +87 -83
- data/docs/deepdive/features/builtin_tools.md +1 -1
- data/docs/deepdive/features/concurrency.md +15 -10
- data/docs/deepdive/features/database.md +19 -19
- data/docs/deepdive/features/embeddings.md +4 -4
- data/docs/deepdive/features/files.md +7 -5
- data/docs/deepdive/fundamentals/agents.md +13 -8
- data/docs/deepdive/fundamentals/providers.md +1 -1
- data/docs/deepdive/fundamentals/schema.md +2 -2
- data/docs/deepdive/fundamentals/stream.md +8 -8
- data/docs/deepdive/fundamentals/tools.md +14 -14
- data/docs/deepdive/media/ocr.md +1 -1
- data/docs/deepdive/protocols/a2a.md +3 -3
- data/docs/deepdive/protocols/mcp.md +116 -7
- data/docs/deepdive/reference/cost.md +1 -1
- data/docs/deepdive/reference/response.md +1 -1
- data/docs/deepdive/reference/tracer.md +90 -22
- data/docs/deepdive.md +1 -1
- data/lib/llm/agent/interrupt.rb +35 -46
- data/lib/llm/agent/registry.rb +106 -0
- data/lib/llm/agent.rb +242 -268
- data/lib/llm/context.rb +63 -55
- data/lib/llm/function/array.rb +2 -1
- data/lib/llm/function/async/group.rb +2 -2
- data/lib/llm/function/async/task.rb +25 -48
- data/lib/llm/function/fiber/group.rb +2 -2
- data/lib/llm/function/fiber/task.rb +16 -30
- data/lib/llm/function/fork/group.rb +2 -2
- data/lib/llm/function/fork/job.rb +8 -4
- data/lib/llm/function/fork/task.rb +25 -30
- data/lib/llm/function/group.rb +33 -5
- data/lib/llm/function/ractor/group.rb +2 -2
- data/lib/llm/function/ractor/job.rb +8 -6
- data/lib/llm/function/ractor/task.rb +32 -9
- data/lib/llm/function/sequential/group.rb +2 -1
- data/lib/llm/function/sequential/task.rb +8 -1
- data/lib/llm/function/task.rb +52 -4
- data/lib/llm/function/thread/group.rb +2 -2
- data/lib/llm/function/thread/task.rb +36 -10
- data/lib/llm/function/window.rb +16 -0
- data/lib/llm/function.rb +14 -43
- data/lib/llm/intercept/multi.rb +40 -0
- data/lib/llm/intercept/null.rb +18 -0
- data/lib/llm/intercept.rb +96 -0
- data/lib/llm/message.rb +1 -1
- data/lib/llm/provider.rb +16 -1
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -1
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -1
- data/lib/llm/providers/deepseek/files.rb +49 -0
- data/lib/llm/providers/deepseek/request_adapter/completion.rb +15 -13
- data/lib/llm/providers/deepseek.rb +16 -2
- data/lib/llm/providers/google/stream_parser.rb +1 -1
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -1
- data/lib/llm/providers/openai/stream_parser.rb +1 -1
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +1 -1
- data/lib/llm/tools/utils.rb +1 -1
- data/lib/llm/tracer/logger.rb +42 -1
- data/lib/llm/tracer/multi.rb +144 -0
- data/lib/llm/tracer/null.rb +20 -0
- data/lib/llm/tracer/pretty_logger.rb +33 -2
- data/lib/llm/tracer/rescue.rb +110 -0
- data/lib/llm/tracer/telemetry.rb +69 -4
- data/lib/llm/tracer.rb +125 -48
- data/lib/llm/utils.rb +12 -2
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +36 -1
- data/llm.gemspec +9 -4
- metadata +16 -35
- data/lib/llm/function/tracing.rb +0 -21
- data/lib/llm/guard/null.rb +0 -19
- data/lib/llm/guard.rb +0 -55
|
@@ -100,7 +100,7 @@ at the end of a session to keep a spend trail.
|
|
|
100
100
|
[`LLM::Cost#to_s`](https://r.uby.dev/api-docs/llm.rb/LLM/Cost.html#to_s-instance_method)
|
|
101
101
|
returns the total in a compact, human-friendly format, rounded to
|
|
102
102
|
two decimals (`"0.01"`). Components that were not used are `0`, so
|
|
103
|
-
you never need to
|
|
103
|
+
you never need to check for `nil` when aggregating.
|
|
104
104
|
|
|
105
105
|
The console renders context usage as a proportion, not a cost.
|
|
106
106
|
[`LLM::Context#context_usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#context_usage-instance_method)
|
|
@@ -16,7 +16,7 @@ their own.
|
|
|
16
16
|
|
|
17
17
|
`LLM::Response` carries the transport response and a little common
|
|
18
18
|
state: `#res` (the underlying HTTP response), `#body` (the parsed body,
|
|
19
|
-
or a raw string), `#ok?`, `#id`, and `#file?`. A name the
|
|
19
|
+
or a raw string), `#ok?`, `#id`, and `#file?`. A name the superclass
|
|
20
20
|
does not define falls through to the body, so a provider field such as
|
|
21
21
|
`content` is reachable directly.
|
|
22
22
|
|
|
@@ -134,6 +134,60 @@ OpenTelemetry.
|
|
|
134
134
|
The tracer can also write to a file with the `path:` option to
|
|
135
135
|
[`LLM::Tracer::Logger.new`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Logger.html#initialize-instance_method).
|
|
136
136
|
|
|
137
|
+
### Multi
|
|
138
|
+
|
|
139
|
+
#### Overview
|
|
140
|
+
|
|
141
|
+
[`LLM::Tracer::Multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Multi.html)
|
|
142
|
+
fans every tracing callback out to a set of tracers, so one request can
|
|
143
|
+
be logged to a file and exported to OpenTelemetry at the same time
|
|
144
|
+
rather than choosing between them.
|
|
145
|
+
|
|
146
|
+
#### How it works
|
|
147
|
+
|
|
148
|
+
[`LLM::Tracer.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#multi-class_method)
|
|
149
|
+
builds a `Multi` from a provider and the tracers to fan out to. The
|
|
150
|
+
provider comes first and is required, so a tracer that is given as a class
|
|
151
|
+
is built with it, while one that is given as an instance is used as it is:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
require "llm"
|
|
155
|
+
|
|
156
|
+
llm = LLM.deepseek(key: ENV["KEY"])
|
|
157
|
+
llm.tracer = LLM::Tracer.multi(
|
|
158
|
+
llm,
|
|
159
|
+
LLM::Tracer::Logger,
|
|
160
|
+
LLM::Tracer.telemetry(llm)
|
|
161
|
+
)
|
|
162
|
+
agent = LLM::Agent.new(llm)
|
|
163
|
+
agent.talk "Hello"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`LLM::Tracer::Multi.new` takes the tracers themselves, so one can also be
|
|
167
|
+
built by hand when a tracer has to be constructed some other way.
|
|
168
|
+
|
|
169
|
+
`on_request_start` and `on_tool_start` return an array with one span per
|
|
170
|
+
tracer, in the same order as
|
|
171
|
+
[`LLM::Tracer::Multi#tracers`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Multi.html#tracers-instance_method).
|
|
172
|
+
Pass that array back as `span:` to the matching finish, error, or
|
|
173
|
+
interrupt callback and each tracer is handed the span it returned. A
|
|
174
|
+
tracer that raises is reported the way
|
|
175
|
+
[`LLM::Tracer::Rescue`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Rescue.html)
|
|
176
|
+
reports any other, and the remaining tracers still run.
|
|
177
|
+
|
|
178
|
+
#### Why would I use it?
|
|
179
|
+
|
|
180
|
+
It is how you keep a human-readable log and a machine-readable trace from
|
|
181
|
+
diverging: one tracer sees the events in development, another sees them
|
|
182
|
+
in production, and neither has to know about the other.
|
|
183
|
+
|
|
184
|
+
#### Notes
|
|
185
|
+
|
|
186
|
+
A `Multi` holds no provider, so `spans` and `flush!` are its own rather
|
|
187
|
+
than any child's: the callbacks are fanned out, but a span list or a
|
|
188
|
+
flush is asked of the tracer that owns it. `on_exit` is fanned out like
|
|
189
|
+
the rest, so every tracer it holds is told when the last scope ends.
|
|
190
|
+
|
|
137
191
|
### Hooks
|
|
138
192
|
|
|
139
193
|
#### Overview
|
|
@@ -153,8 +207,13 @@ for that request, so a tracer can correlate its events even when a
|
|
|
153
207
|
turn makes several requests.
|
|
154
208
|
|
|
155
209
|
Three more hooks cover a local tool call. `on_tool_start` fires before
|
|
156
|
-
the tool runs and returns the span that `on_tool_finish
|
|
157
|
-
`on_tool_error` receive.
|
|
210
|
+
the tool runs and returns the span that `on_tool_finish`,
|
|
211
|
+
`on_tool_error`, and `on_tool_interrupt` receive. Each ending is also
|
|
212
|
+
handed a `started_at:` and a `finished_at:`, the monotonic clock times
|
|
213
|
+
the call opened and ended at. The endings are delivered in the order
|
|
214
|
+
the group holds its calls, so a call that finished first can still be
|
|
215
|
+
announced after one that finished later. Those two times, and not the
|
|
216
|
+
moment the callback runs, are what say how long a call took.
|
|
158
217
|
|
|
159
218
|
An interrupt is neither a finish nor an error, and it is reported to a
|
|
160
219
|
hook of its own. `on_interrupt` is called with a `scope:` of `:request`,
|
|
@@ -167,6 +226,12 @@ between a turn's requests, where there was nothing more precise to
|
|
|
167
226
|
interrupt. A cancel reaches every tool that is running, so the tool phase
|
|
168
227
|
is announced once rather than once per tool.
|
|
169
228
|
|
|
229
|
+
A tool call that was interrupted is closed through `on_tool_interrupt`
|
|
230
|
+
as well, which is handed the exception the caller is given and the span
|
|
231
|
+
`on_tool_start` returned. That span is the only thing that can name a
|
|
232
|
+
call which never returned a result, so a tracer can pair a start with an
|
|
233
|
+
end rather than leaving the span open.
|
|
234
|
+
|
|
170
235
|
A tracer's own lifetime is bracketed as well. `on_exit` fires once,
|
|
171
236
|
when the last scope that is open for that tracer ends. That scope can
|
|
172
237
|
belong to a different thread than the one that opened the first: a tool
|
|
@@ -184,7 +249,7 @@ and a tracer that supports it (such as
|
|
|
184
249
|
uses that id to give every span of the turn the same trace id:
|
|
185
250
|
|
|
186
251
|
```ruby
|
|
187
|
-
class MyTracer < LLM::Tracer
|
|
252
|
+
class MyTracer < LLM::Tracer::Null
|
|
188
253
|
def on_request_start(operation:, model: nil, **)
|
|
189
254
|
warn "start #{operation} #{model}"
|
|
190
255
|
end
|
|
@@ -193,20 +258,12 @@ class MyTracer < LLM::Tracer
|
|
|
193
258
|
warn "finish #{operation}"
|
|
194
259
|
end
|
|
195
260
|
|
|
196
|
-
def on_request_error(ex:, **)
|
|
197
|
-
warn "error #{ex.class}"
|
|
198
|
-
end
|
|
199
|
-
|
|
200
261
|
def on_tool_start(id:, name:, arguments:, model:, **)
|
|
201
262
|
warn "tool #{name}"
|
|
202
263
|
end
|
|
203
264
|
|
|
204
|
-
def
|
|
205
|
-
warn "tool #{
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def on_tool_error(ex:, **)
|
|
209
|
-
warn "tool error #{ex.class}"
|
|
265
|
+
def on_tool_interrupt(ex:, span:, **)
|
|
266
|
+
warn "tool interrupted: #{ex.class}"
|
|
210
267
|
end
|
|
211
268
|
end
|
|
212
269
|
|
|
@@ -225,15 +282,26 @@ as hooks gain parameters.
|
|
|
225
282
|
|
|
226
283
|
#### Notes
|
|
227
284
|
|
|
228
|
-
The
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
285
|
+
The superclass raises `NotImplementedError` for every hook the runtime
|
|
286
|
+
calls, so subclassing
|
|
287
|
+
[`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
|
|
288
|
+
means answering all of them: the six request and tool hooks above,
|
|
289
|
+
`on_tool_interrupt`, `on_interrupt`, and `on_exit`.
|
|
290
|
+
[`LLM::Tracer::Null`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Null.html)
|
|
291
|
+
answers them all with a no-op, which is the shorter route when you want
|
|
292
|
+
only a few, and what the example above takes. `start_trace` and
|
|
293
|
+
`stop_trace` are the exception: their base implementations are no-ops
|
|
294
|
+
rather than raises, because a tracer that keeps no trace has nothing to
|
|
295
|
+
open or close.
|
|
296
|
+
|
|
297
|
+
A hook that raises does not take the turn with it.
|
|
298
|
+
[`LLM::Tracer::Rescue`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Rescue.html)
|
|
299
|
+
is prepended onto every subclass of `LLM::Tracer`, so the raise is
|
|
300
|
+
reported on standard error and the turn carries on. `LLM::Interrupt` is
|
|
301
|
+
re-raised, since a cancel is not a tracer's to swallow.
|
|
302
|
+
|
|
303
|
+
Accept `**` to absorb keywords you do not read, as the built-in tracers
|
|
304
|
+
do, so a hook that gains a parameter does not break your subclass.
|
|
237
305
|
|
|
238
306
|
### PrettyLogger
|
|
239
307
|
|
data/docs/deepdive.md
CHANGED
|
@@ -81,7 +81,7 @@ is the best place to start if you are new to llm.rb.
|
|
|
81
81
|
- [Cancellation](deepdive/advanced/cancellation.md)
|
|
82
82
|
- [Transports](deepdive/advanced/transports.md)
|
|
83
83
|
- [Transformer](deepdive/advanced/transformer.md)
|
|
84
|
-
- [
|
|
84
|
+
- [Intercept](deepdive/advanced/intercept.md)
|
|
85
85
|
|
|
86
86
|
## Media
|
|
87
87
|
|
data/lib/llm/agent/interrupt.rb
CHANGED
|
@@ -2,63 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
class LLM::Agent
|
|
4
4
|
##
|
|
5
|
-
#
|
|
5
|
+
# Stop an agent in its tracks.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
7
|
+
# The {LLM::Agent::Interrupt} module extends
|
|
8
|
+
# the instance variable `@caller` (an instance
|
|
9
|
+
# of {LLM::Object}) that belongs to an agent
|
|
10
|
+
# and its context.
|
|
11
|
+
#
|
|
12
|
+
# The `@caller` instance variable captures the
|
|
13
|
+
# agent's thread, fiber, and scheduler. The
|
|
14
|
+
# variable is in scope for as long as an agent
|
|
15
|
+
# is running and it provides other threads and
|
|
16
|
+
# fibers with access to the agent's thread,
|
|
17
|
+
# fiber and scheduler.
|
|
18
|
+
#
|
|
19
|
+
# A different thread or fiber can use this module
|
|
20
|
+
# and the object it extends (`@caller`) to stop
|
|
21
|
+
# an agent in its tracks via the raise of
|
|
22
|
+
# {LLM::Interrupt LLM::Interrupt} on the
|
|
23
|
+
# agent's thread, or fiber.
|
|
24
|
+
#
|
|
25
|
+
# @note
|
|
26
|
+
# The method provided by this module is
|
|
27
|
+
# not called directly, and the `@caller`
|
|
28
|
+
# instance variable is private. The
|
|
29
|
+
# {LLM::Agent#interrupt!} and {LLM::Context#interrupt!}
|
|
30
|
+
# methods call it on your behalf.
|
|
15
31
|
#
|
|
16
|
-
# It is public rather than private because the runtime is not the only
|
|
17
|
-
# thing that runs a loop: an application with a loop of its own can record
|
|
18
|
-
# a caller the same way, and a spec can build one without running a turn.
|
|
19
32
|
# @see LLM::Agent#run_loop
|
|
20
33
|
# @see LLM::Context#interrupt!
|
|
21
34
|
module Interrupt
|
|
22
35
|
##
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# Which name receives the raise is decided by who is cancelling:
|
|
26
|
-
#
|
|
27
|
-
# another thread the thread, because a fiber belongs to the thread
|
|
28
|
-
# that made it and cannot be entered from another
|
|
29
|
-
# one. The raise lands in whichever fiber that
|
|
30
|
-
# thread is running, which is the turn's, because
|
|
31
|
-
# the turn is what it is doing.
|
|
32
|
-
#
|
|
33
|
-
# the same thread the fiber, because raising on the thread would
|
|
34
|
-
# raise in the canceller that asked for it.
|
|
36
|
+
# Stops an agent in its tracks.
|
|
35
37
|
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# {LLM::Function::Fiber::Task#interrupt!} asks, because a direct raise
|
|
41
|
-
# into a scheduled fiber does not transfer: it suspends the thread that
|
|
42
|
-
# raises, and that thread is the canceller's. A fiber with no scheduler
|
|
43
|
-
# behind it - a turn an application ran in a fiber of its own - is raised
|
|
44
|
-
# into directly, which is what such a fiber is for.
|
|
38
|
+
# When this method is called from
|
|
39
|
+
# the agent's thread but a different
|
|
40
|
+
# fiber an interrupt is raised on the
|
|
41
|
+
# caller's fiber.
|
|
45
42
|
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# the turn as it unwinds - is not available: between two requests there
|
|
51
|
-
# is no frame of the turn's own to announce it from.
|
|
43
|
+
# And when it is called from a thread
|
|
44
|
+
# other than the agent's thread, the
|
|
45
|
+
# interrupt is raised on the caller's
|
|
46
|
+
# thread.
|
|
52
47
|
#
|
|
53
|
-
#
|
|
54
|
-
# raise into - and nothing is raised when the turn ended between the read
|
|
55
|
-
# and the raise, which is the ordinary race, because a cancel that
|
|
56
|
-
# arrives after a turn is not a failure.
|
|
57
|
-
# @return [nil]
|
|
48
|
+
# @return [void]
|
|
58
49
|
def interrupt!
|
|
59
50
|
if thread.equal?(Thread.current)
|
|
60
51
|
if fiber
|
|
61
|
-
tracer&.on_interrupt(scope: :agent)
|
|
62
52
|
if scheduler
|
|
63
53
|
scheduler.fiber_interrupt(fiber, LLM::Interrupt.new("agent interrupted"))
|
|
64
54
|
else
|
|
@@ -66,7 +56,6 @@ class LLM::Agent
|
|
|
66
56
|
end
|
|
67
57
|
end
|
|
68
58
|
else
|
|
69
|
-
tracer&.on_interrupt(scope: :agent)
|
|
70
59
|
thread.raise(LLM::Interrupt, "agent interrupted")
|
|
71
60
|
end
|
|
72
61
|
nil
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Agent
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Agent::Registry LLM::Agent::Registry} holds the agents that are
|
|
6
|
+
# running a turn in this process, so that a cancel can find one.
|
|
7
|
+
#
|
|
8
|
+
# Delivery is not the problem it solves. `LLM::Agent#interrupt!` can be
|
|
9
|
+
# called from any thread, and {LLM::Agent::Interrupt LLM::Agent::Interrupt}
|
|
10
|
+
# decides what receives the raise - but a canceller has to hold the agent,
|
|
11
|
+
# and in an application the agent is built inside the job that runs the
|
|
12
|
+
# turn and stays a local variable for as long as the turn lasts. A cancel
|
|
13
|
+
# button has nothing to reach.
|
|
14
|
+
#
|
|
15
|
+
# So a turn registers itself for as long as it runs, and a canceller names
|
|
16
|
+
# it by the agent, or by the id it already has: the record's id when the
|
|
17
|
+
# agent was built around one, the agent's own otherwise.
|
|
18
|
+
#
|
|
19
|
+
# ## What it does not promise
|
|
20
|
+
#
|
|
21
|
+
# It is **one process**, and it holds the agents *that process* is running.
|
|
22
|
+
# A cancel that lands elsewhere - a second worker, a deploy that leaves two
|
|
23
|
+
# up for a minute - finds nothing, and `false` must not be read as "the turn
|
|
24
|
+
# is over". An application that needs a guarantee keeps one of its own; this
|
|
25
|
+
# is the fast path.
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
class Registry
|
|
29
|
+
##
|
|
30
|
+
# @return [LLM::Agent::Registry]
|
|
31
|
+
def initialize
|
|
32
|
+
@agents = {}
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# Records that an agent is running a turn.
|
|
38
|
+
#
|
|
39
|
+
# Strongly, and that is the point: an entry has to reach an agent that
|
|
40
|
+
# is blocked inside `talk`, so a weak reference would let a turn that is
|
|
41
|
+
# running be collected. The lifetime is the caller's `ensure`.
|
|
42
|
+
# @param [LLM::Agent] agent
|
|
43
|
+
# @return [LLM::Agent]
|
|
44
|
+
# Returns the agent
|
|
45
|
+
def enter(agent)
|
|
46
|
+
@mutex.synchronize { @agents[key(agent)] = agent }
|
|
47
|
+
agent
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Records that an agent's turn is over.
|
|
52
|
+
#
|
|
53
|
+
# Removed only if it is still the agent that registered. Two turns under
|
|
54
|
+
# one identity - a second attempt alongside one that is finishing - are
|
|
55
|
+
# not the same turn, and the one that finishes first must not take the
|
|
56
|
+
# other one's registration with it.
|
|
57
|
+
# @param [LLM::Agent] agent
|
|
58
|
+
# @return [void]
|
|
59
|
+
def exit(agent)
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
id = key(agent)
|
|
62
|
+
@agents.delete(id) if @agents[id].equal?(agent)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# The agent running a turn under this identity, or nil.
|
|
68
|
+
#
|
|
69
|
+
# Named by the agent or by the id, and not by both: the two are the same
|
|
70
|
+
# question asked with different things in hand. An id is compared with
|
|
71
|
+
# `==`, so a host may pass a string, a number, or whatever it holds that
|
|
72
|
+
# equals the id a turn registered under.
|
|
73
|
+
#
|
|
74
|
+
# Looked up under the lock and answered outside it, because the interrupt
|
|
75
|
+
# that follows is not this class's to make: a caller holding the lock
|
|
76
|
+
# while raising into another thread's turn would be holding it against
|
|
77
|
+
# that turn's own `exit`.
|
|
78
|
+
# @param [LLM::Agent, nil] agent
|
|
79
|
+
# @param [String, Integer, nil] id
|
|
80
|
+
# @raise [ArgumentError]
|
|
81
|
+
# When neither is given, or when both are
|
|
82
|
+
# @return [LLM::Agent, nil]
|
|
83
|
+
def find(agent: nil, id: nil)
|
|
84
|
+
if (agent.nil? and id.nil?) or (!agent.nil? and !id.nil?)
|
|
85
|
+
raise ArgumentError, "provide an agent or an id, and not both"
|
|
86
|
+
else
|
|
87
|
+
@mutex.synchronize { @agents[agent ? key(agent) : id] }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# The identity an agent registers under.
|
|
95
|
+
#
|
|
96
|
+
# The record's id when the agent was built around a record, because that
|
|
97
|
+
# is what a host already has - a row it can name from a route - and the
|
|
98
|
+
# agent's own otherwise. One value, not two: an agent with a record
|
|
99
|
+
# answers to the record's id and not also to its own.
|
|
100
|
+
# @param [LLM::Agent] agent
|
|
101
|
+
# @return [String, Integer]
|
|
102
|
+
def key(agent)
|
|
103
|
+
agent.record&.id || agent.id
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|