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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +535 -2
  3. data/README.md +857 -501
  4. data/data/alibaba.json +2 -2
  5. data/data/anthropic.json +66 -3
  6. data/data/bedrock.json +781 -12
  7. data/data/deepinfra.json +157 -1
  8. data/data/google.json +48 -0
  9. data/data/mistral.json +326 -16
  10. data/data/moonshot.json +2 -1
  11. data/data/openai.json +13 -0
  12. data/data/openrouter.json +1078 -1235
  13. data/data/xai.json +4 -2
  14. data/docs/deepdive/advanced/cancellation.md +35 -7
  15. data/docs/deepdive/advanced/compaction.md +5 -5
  16. data/docs/deepdive/advanced/context.md +45 -24
  17. data/docs/deepdive/advanced/{guard.md → intercept.md} +87 -83
  18. data/docs/deepdive/features/builtin_tools.md +1 -1
  19. data/docs/deepdive/features/concurrency.md +15 -10
  20. data/docs/deepdive/features/database.md +19 -19
  21. data/docs/deepdive/features/embeddings.md +4 -4
  22. data/docs/deepdive/features/files.md +7 -5
  23. data/docs/deepdive/fundamentals/agents.md +13 -8
  24. data/docs/deepdive/fundamentals/providers.md +1 -1
  25. data/docs/deepdive/fundamentals/schema.md +2 -2
  26. data/docs/deepdive/fundamentals/stream.md +8 -8
  27. data/docs/deepdive/fundamentals/tools.md +14 -14
  28. data/docs/deepdive/media/ocr.md +1 -1
  29. data/docs/deepdive/protocols/a2a.md +3 -3
  30. data/docs/deepdive/protocols/mcp.md +116 -7
  31. data/docs/deepdive/reference/cost.md +1 -1
  32. data/docs/deepdive/reference/response.md +1 -1
  33. data/docs/deepdive/reference/tracer.md +90 -22
  34. data/docs/deepdive.md +1 -1
  35. data/lib/llm/agent/interrupt.rb +35 -46
  36. data/lib/llm/agent/registry.rb +106 -0
  37. data/lib/llm/agent.rb +242 -268
  38. data/lib/llm/context.rb +63 -55
  39. data/lib/llm/function/array.rb +2 -1
  40. data/lib/llm/function/async/group.rb +2 -2
  41. data/lib/llm/function/async/task.rb +25 -48
  42. data/lib/llm/function/fiber/group.rb +2 -2
  43. data/lib/llm/function/fiber/task.rb +16 -30
  44. data/lib/llm/function/fork/group.rb +2 -2
  45. data/lib/llm/function/fork/job.rb +8 -4
  46. data/lib/llm/function/fork/task.rb +25 -30
  47. data/lib/llm/function/group.rb +33 -5
  48. data/lib/llm/function/ractor/group.rb +2 -2
  49. data/lib/llm/function/ractor/job.rb +8 -6
  50. data/lib/llm/function/ractor/task.rb +32 -9
  51. data/lib/llm/function/sequential/group.rb +2 -1
  52. data/lib/llm/function/sequential/task.rb +8 -1
  53. data/lib/llm/function/task.rb +52 -4
  54. data/lib/llm/function/thread/group.rb +2 -2
  55. data/lib/llm/function/thread/task.rb +36 -10
  56. data/lib/llm/function/window.rb +16 -0
  57. data/lib/llm/function.rb +14 -43
  58. data/lib/llm/intercept/multi.rb +40 -0
  59. data/lib/llm/intercept/null.rb +18 -0
  60. data/lib/llm/intercept.rb +96 -0
  61. data/lib/llm/message.rb +1 -1
  62. data/lib/llm/provider.rb +16 -1
  63. data/lib/llm/providers/anthropic/stream_parser.rb +1 -1
  64. data/lib/llm/providers/bedrock/stream_parser.rb +1 -1
  65. data/lib/llm/providers/deepseek/files.rb +49 -0
  66. data/lib/llm/providers/deepseek/request_adapter/completion.rb +15 -13
  67. data/lib/llm/providers/deepseek.rb +16 -2
  68. data/lib/llm/providers/google/stream_parser.rb +1 -1
  69. data/lib/llm/providers/openai/responses/stream_parser.rb +1 -1
  70. data/lib/llm/providers/openai/stream_parser.rb +1 -1
  71. data/lib/llm/stream.rb +8 -7
  72. data/lib/llm/tool.rb +1 -1
  73. data/lib/llm/tools/utils.rb +1 -1
  74. data/lib/llm/tracer/logger.rb +42 -1
  75. data/lib/llm/tracer/multi.rb +144 -0
  76. data/lib/llm/tracer/null.rb +20 -0
  77. data/lib/llm/tracer/pretty_logger.rb +33 -2
  78. data/lib/llm/tracer/rescue.rb +110 -0
  79. data/lib/llm/tracer/telemetry.rb +69 -4
  80. data/lib/llm/tracer.rb +125 -48
  81. data/lib/llm/utils.rb +12 -2
  82. data/lib/llm/version.rb +1 -1
  83. data/lib/llm.rb +36 -1
  84. data/llm.gemspec +9 -4
  85. metadata +16 -35
  86. data/lib/llm/function/tracing.rb +0 -21
  87. data/lib/llm/guard/null.rb +0 -19
  88. 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 guard against `nil` when aggregating.
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 base class
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` and
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 on_tool_finish(result:, **)
205
- warn "tool #{result.name} done"
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 base class raises `NotImplementedError` for the six request and tool
229
- hooks above, so a tracer has to answer for each of them. The remaining
230
- hooks do nothing by default. `on_exit` does nothing because most tracers
231
- hold nothing that needs releasing, and `start_trace` and `stop_trace`
232
- are no-ops too. `on_interrupt` does nothing because an interrupt is
233
- delivered to whatever tracer happens to be bound, and a hook that raised
234
- would replace the interrupt every caller is written against. Accept `**`
235
- to absorb keywords you do not read, as the built-in tracers do, so a
236
- hook that gains a parameter does not break your subclass.
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
- - [Guard](deepdive/advanced/guard.md)
84
+ - [Intercept](deepdive/advanced/intercept.md)
85
85
 
86
86
  ## Media
87
87
 
@@ -2,63 +2,53 @@
2
2
 
3
3
  class LLM::Agent
4
4
  ##
5
- # What a turn's caller answers to.
5
+ # Stop an agent in its tracks.
6
6
  #
7
- # A turn is a loop, and between two of its requests there is nothing in
8
- # flight to close and nothing running to raise into. {LLM::Agent#run_loop}
9
- # records where it is running - the thread, the fiber, the scheduler that
10
- # fiber belongs to, and the tracer the turn is traced with - and extends
11
- # this onto that record, so an interrupt with nothing more precise to do
12
- # has somewhere to land: {LLM::Context#interrupt!} calls `interrupt!` on
13
- # the caller it holds, and what the caller does with the names is its own
14
- # business.
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
- # Ends the turn where it is, and tells the tracer first.
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
- # A fiber scheduler is the case the second rule is for: a turn under
37
- # Falcon or Async runs on a fiber of the reactor's thread, and a cancel
38
- # that arrives on that thread is another fiber asking. Such a fiber is
39
- # asked for through the scheduler, the way
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
- # The tracer is told `scope: :agent`, before the raise, the way
47
- # {LLM::Context#wait} tells it `scope: :tool` before the caller is given
48
- # that interrupt. A tracer that has to account for every interrupt a turn
49
- # receives needs this one too, and the alternative - announcing it from
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
- # Nothing is raised, and nothing is announced, when there is no fiber to
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