llm.rb 16.0.0 → 17.0.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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +510 -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 +244 -269
  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
data/data/xai.json CHANGED
@@ -83,7 +83,8 @@
83
83
  "modalities": {
84
84
  "input": [
85
85
  "text",
86
- "image"
86
+ "image",
87
+ "pdf"
87
88
  ],
88
89
  "output": [
89
90
  "video"
@@ -122,7 +123,8 @@
122
123
  "limit": {
123
124
  "context": 16000,
124
125
  "output": 0
125
- }
126
+ },
127
+ "canonical_model_id": "xai/grok-imagine-image"
126
128
  },
127
129
  "grok-imagine-video": {
128
130
  "id": "grok-imagine-video",
@@ -14,17 +14,17 @@ the tokens.
14
14
  #### How it works
15
15
 
16
16
  When you want to cancel an active request, tool call, or turn, call
17
- [`LLM::Agent#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#interrupt!)
17
+ [`LLM::Agent#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#interrupt!-instance_method)
18
18
  or
19
- [`LLM::Context#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#interrupt!)
19
+ [`LLM::Context#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#interrupt!-instance_method)
20
20
  from any thread. Three things happen, and the last of them is what
21
21
  reaches a turn that is doing nothing in particular:
22
22
 
23
23
  [`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
24
24
  is raised on the thread where
25
- [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk)
25
+ [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk-instance_method)
26
26
  or
27
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
27
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
28
28
  is running, so the caller
29
29
  can rescue it and know the request was cancelled.
30
30
 
@@ -43,7 +43,7 @@ And when the turn is between two requests - waiting out a retry,
43
43
  handing tool returns back to the model, or building the next request -
44
44
  there is nothing in flight to close and nothing running to raise into,
45
45
  so the interrupt is raised into the frame the turn is running in.
46
- [`LLM::Agent#run_loop`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#run_loop)
46
+ [`LLM::Agent#run_loop`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#run_loop-instance_method)
47
47
  records that frame for as long as the turn lasts: the thread when the
48
48
  canceller is another thread, and the fiber when the canceller shares the
49
49
  turn's thread. That second case is a fiber scheduler - a turn under
@@ -81,6 +81,13 @@ a rate-limited turn waiting out its backoff is a turn that has not been
81
81
  stopped until the cancel reaches it, and the request it is waiting to
82
82
  make is one nobody asked for any more.
83
83
 
84
+ It is also worth saying what is unusual about it. The usual shape of a
85
+ cancel is a request that is aborted: this one reaches a tool that is
86
+ running, and a turn that is between two requests. And a cancel does not
87
+ leave a hole: the call it stopped is closed with an in-band return
88
+ before the next request goes out, so the conversation the model sees
89
+ stays valid and the model is told what happened rather than finding one.
90
+
84
91
  #### Notes
85
92
 
86
93
  How an interrupt reaches a tool depends on the strategy. The
@@ -95,9 +102,18 @@ reach. The `:sequential` strategy has nothing to raise into: its
95
102
  tool runs on the caller's own thread, so a cancel tells the tool
96
103
  through its hook alone.
97
104
 
105
+ Where the hook runs against the raise is the one thing a tool can
106
+ feel twice. On `:fork` and `:ractor` the hook is written before the
107
+ raise, so a tool that releases a resource in the hook has released it
108
+ by the time the interrupt arrives. On `:thread`, `:fiber` and `:async`
109
+ it runs in the call's `ensure`, after the raise has landed and after
110
+ the tool's own `rescue` - so a tool that cleans up in both places
111
+ cleans up twice, rescue first, and one that cleans up in one place
112
+ should pick the hook.
113
+
98
114
  A cancel is delivered at the point the turn has reached, which can be
99
115
  any point of it: inside a stream callback, a compactor, a transformer,
100
- a guard, or the persistence write a step makes.
116
+ an intercept, or the persistence write a step makes.
101
117
  [`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
102
118
  sits outside `StandardError`, so neither a bare `rescue` nor a
103
119
  `rescue => ex` catches it, and the language is what keeps a cancel from
@@ -107,10 +123,22 @@ does; it is not an `LLM::Error` either, so a rescue of the runtime's
107
123
  error superclass does not catch it.
108
124
 
109
125
  The runtime's own path out of a turn does not swallow an interrupt:
110
- [`LLM::Context#try`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#try)
126
+ [`LLM::Context#try`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#try-instance_method)
111
127
  re-raises anything that is not a retryable failure, and the tracer
112
128
  interface has no broad rescues of its own.
113
129
 
130
+ A turn can also be reached from outside the code that started it.
131
+ [`LLM.interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM.html#interrupt-class_method)
132
+ finds the turn running under an agent, an agent's own id, or a record's
133
+ id and interrupts it, so a controller, a job, or a socket handler can
134
+ stop a turn it never held a reference to. A turn registers itself through
135
+ [`LLM::Agent.registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#registry-class_method)
136
+ while it runs and is forgotten when it ends, so the method answers
137
+ `false` when nothing was registered under that name, which is the
138
+ ordinary race rather than a failure. It takes `attempts:` and `interval:`
139
+ to poll for a turn that has not started yet, and the registry is per
140
+ process, so a cancel that lands in another worker finds nothing.
141
+
114
142
  The names are worth separating.
115
143
  [`LLM::Context#interrupt!`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#interrupt!-instance_method)
116
144
  and
@@ -9,14 +9,14 @@ Long-running conversations consume tokens. Without intervention,
9
9
  every turn pushes toward the model's context window limit. Compaction
10
10
  drops old messages to keep the conversation alive. The runtime runs
11
11
  a compactor automatically before each
12
- [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk)
12
+ [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk-instance_method)
13
13
  call, trimming the oldest messages when the conversation exceeds a configured size.
14
14
  This keeps the context window healthy without manual intervention.
15
15
 
16
16
  #### How it works
17
17
 
18
18
  Compactors run automatically before each
19
- [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk)
19
+ [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk-instance_method)
20
20
  call.
21
21
  [`LLM::Compactor::Truncate`](https://r.uby.dev/api-docs/llm.rb/LLM/Compactor/Truncate.html)
22
22
  strategy drops the oldest messages, keeping only the N most recent.
@@ -77,11 +77,11 @@ trade-off is that dropped messages are gone, so information may be
77
77
  lost. Set `keep` to a higher number to retain more context.
78
78
 
79
79
  Both strategies accept
80
- [`LLM::Stream#on_compaction`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction)
80
+ [`LLM::Stream#on_compaction`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction-instance_method)
81
81
  and
82
- [`LLM::Stream#on_compaction_finish`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction_finish)
82
+ [`LLM::Stream#on_compaction_finish`](https://r.uby.dev/api-docs/llm.rb/LLM/Stream.html#on_compaction_finish-instance_method)
83
83
  stream callbacks so the UI can show progress, but only the `Truncate`
84
84
  strategy emits them. `Null` performs no compaction and emits nothing.
85
85
  The context's
86
- [`LLM::Context#compacted?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#compacted?)
86
+ [`LLM::Context#compacted?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#compacted?-instance_method)
87
87
  flag is `true` between compaction and the next model response.
@@ -7,7 +7,7 @@
7
7
 
8
8
  [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
9
9
  is the runtime that powers every agent. When you call
10
- [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk),
10
+ [`LLM::Agent#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#talk-instance_method),
11
11
  the agent delegates to its internal context. The context manages
12
12
  the message history, sends requests to the provider, tracks pending
13
13
  tool calls, and feeds results back to the model. Everything an agent
@@ -23,22 +23,22 @@ any workflow where the agent's automatic loop gets in the way.
23
23
 
24
24
  A context wraps a provider and maintains the conversation state.
25
25
  Call
26
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
26
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
27
27
  to send input to the model, check
28
- [`LLM::Context#pending_functions?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions?)
28
+ [`LLM::Context#pending_functions?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions?-instance_method)
29
29
  to see if tools were requested, and use
30
- [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait)
30
+ [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait-instance_method)
31
31
  to execute them. Each call to
32
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
32
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
33
33
  appends
34
34
  to the conversation and returns the model's response. The context
35
35
  serializes its state with
36
- [`LLM::Context#to_h`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#to_h)
36
+ [`LLM::Context#to_h`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#to_h-instance_method)
37
37
  and
38
- [`LLM::Context#to_json`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#to_json),
38
+ [`LLM::Context#to_json`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#to_json-instance_method),
39
39
  and restores it
40
40
  with
41
- [`LLM::Context#restore`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#restore).
41
+ [`LLM::Context#restore`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#restore-instance_method).
42
42
  This is how the ORM integrations and filesystem
43
43
  persistence work under the hood:
44
44
 
@@ -120,6 +120,13 @@ and
120
120
  are read from the id rather than stored, so they return `nil` when
121
121
  the id is not a UUIDv7 string.
122
122
 
123
+ A context bound to a record takes that record's id when the record's
124
+ id is a UUIDv7 string, and generates one otherwise - so an agent's
125
+ id is the identity its host already has, and a record with an
126
+ integer primary key, or a row that is not saved yet, still gets a
127
+ UUIDv7 of its own. Pass `id:` to name a context yourself; it must be
128
+ a UUIDv7 string, and anything else raises `LLM::Error`.
129
+
123
130
  #### Why would I use it?
124
131
 
125
132
  An id gives a conversation or a message a stable name you can log,
@@ -132,6 +139,12 @@ creation order and no separate timestamp column is needed.
132
139
  The id is generated once and saved with the runtime state, so it
133
140
  survives a save and a restore. A payload written before ids existed
134
141
  has none, so its object is restored with a fresh id.
142
+ A UUIDv7 is a constraint rather than a convention: an id that is not
143
+ one is refused with `LLM::Error`, because the id is what carries the
144
+ creation time, and the same string is meant to name a row and the
145
+ conversation bound to it.
146
+ [`LLM::Utils.uuidv7?`](https://r.uby.dev/api-docs/llm.rb/LLM/Utils.html#uuidv7?-instance_method)
147
+ is the check.
135
148
  [`LLM::Message#==`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html#==-instance_method)
136
149
  ignores the id, so a difference in creation time alone does not make
137
150
  two messages unequal.
@@ -159,9 +172,9 @@ When you want to control the tool loop yourself, drive
159
172
  directly instead of using an agent. Start a conversation, check
160
173
  for tool requests, execute them, and feed results back. The full
161
174
  loop is under your control. Each call to
162
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
175
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
163
176
  appends to the conversation and returns the model's response, and
164
- [`LLM::Context#pending_functions?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions?)
177
+ [`LLM::Context#pending_functions?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions?-instance_method)
165
178
  tells you whether tools were requested.
166
179
  From that foundation you can inspect, iterate, or confirm
167
180
  per-tool in a single flow:
@@ -201,14 +214,14 @@ recovery that the default tool loop does not expose.
201
214
 
202
215
  #### Notes
203
216
 
204
- [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait)
217
+ [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait-instance_method)
205
218
  picks up pending functions, spawns them using the chosen
206
219
  strategy, waits for results, and records them back in the context.
207
220
  Each strategy is supported: `:sequential`, `:thread`, `:fiber`,
208
221
  `:async`, `:fork`, and `:ractor`. Functions are reset after each
209
- [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait)
222
+ [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait-instance_method)
210
223
  or
211
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
224
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
212
225
  call. Store the array if you need to
213
226
  preserve them.
214
227
 
@@ -217,10 +230,10 @@ preserve them.
217
230
  #### Overview
218
231
 
219
232
  Pending function calls represent the model's tool requests. After
220
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
233
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
221
234
  returns, the context may have pending function
222
235
  calls if the model requested tools. These are available through
223
- [`LLM::Context#pending_functions`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions)
236
+ [`LLM::Context#pending_functions`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions-instance_method)
224
237
  which returns an array of
225
238
  [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
226
239
  objects. Each function has a name, arguments, and methods for
@@ -229,15 +242,15 @@ execution or cancellation.
229
242
  #### How it works
230
243
 
231
244
  When you want to check whether the model requested tools, call
232
- [`LLM::Context#pending_functions?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions?)
245
+ [`LLM::Context#pending_functions?`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#pending_functions?-instance_method)
233
246
  after each
234
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
247
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
235
248
  call. Each pending
236
249
  function has a name, arguments, and
237
250
  methods for execution or cancellation. Call
238
- [`LLM::Function#task`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#task)
251
+ [`LLM::Function#task`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#task-instance_method)
239
252
  to execute it or
240
- [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel)
253
+ [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method)
241
254
  to skip it. Iterate over all
242
255
  pending functions to inspect or handle them individually:
243
256
 
@@ -268,9 +281,9 @@ the model requested.
268
281
  #### Notes
269
282
 
270
283
  Pending functions are reset after each
271
- [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait)
284
+ [`LLM::Context#wait`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#wait-instance_method)
272
285
  or
273
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk)
286
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method)
274
287
  call. If you need to preserve them, store the array before
275
288
  executing. Functions that are cancelled still count as completed
276
289
  from the model's perspective; the model sees a cancellation
@@ -402,7 +415,7 @@ anything else the model supports.
402
415
  Call
403
416
  [`LLM::Context#prompt`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#prompt-instance_method)
404
417
  with a block, then pass the result to
405
- [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk).
418
+ [`LLM::Context#talk`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#talk-instance_method).
406
419
  Inside the block, `system`, `user`, and `developer` append a message
407
420
  with the matching role, and `talk` appends one with an explicit
408
421
  role. The provider resolves each role to its provider-specific name:
@@ -423,7 +436,7 @@ res = ctx.talk(prompt)
423
436
 
424
437
  The block receives the prompt object when it takes an argument, and
425
438
  otherwise runs in the prompt's context:
426
- [`LLM::Prompt#to_a`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#to_a)
439
+ [`LLM::Prompt#to_a`](https://r.uby.dev/api-docs/llm.rb/LLM/Prompt.html#to_a-instance_method)
427
440
  returns the messages in order, and two prompts are equal when their
428
441
  messages match. Each of `system`, `user`, `developer`, and `talk`
429
442
  accepts an `extra:` hash, which is attached to the message it builds,
@@ -495,5 +508,13 @@ model to read, instead of pasting its contents into the prompt.
495
508
  Which files a model accepts depends on the provider and the model.
496
509
  `ask` is a shorthand over `talk`: it builds the same prompt and returns
497
510
  the same `LLM::Response`, so anything that works with `talk` works with
498
- `ask`. An agent delegates all four methods to the context it wraps.
511
+ `ask`. It also accepts a `stream:` target and a block, the way `talk`
512
+ does. An agent delegates all four methods to the context it wraps.
513
+
514
+ A file the provider cannot take is refused with
515
+ [`LLM::PromptError`](https://r.uby.dev/api-docs/llm.rb/LLM/PromptError.html),
516
+ raised while the prompt is built rather than by the tool call. The
517
+ message names what was rejected: DeepSeek has no Files API, so a remote
518
+ file raises there, and it reads images only, so anything else raises for
519
+ its own reason.
499
520
 
@@ -1,41 +1,41 @@
1
1
 
2
- ## Guard
2
+ ## Intercept
3
3
 
4
4
  ### Introduction
5
5
 
6
6
  #### Overview
7
7
 
8
- [`LLM::Guard`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard.html)
9
- is the superclass for context-level supervisors. A guard is bound
10
- to a context and inspects each pending tool call before it runs.
11
- It can let the call through, cancel it, block it with an error, or
12
- answer for it with a synthesized result. Guards handle policy,
13
- validation, quotas, cost control, caching, and approval workflows.
8
+ [`LLM::Intercept`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html)
9
+ is the superclass for context-level supervisors. An intercept is bound
10
+ to a context and inspects each pending tool call before it runs. It
11
+ can let the call through, cancel it, block it with an error, or answer
12
+ for it with a synthesized result. Intercepts handle policy, validation,
13
+ quotas, cost control, caching, and approval workflows.
14
14
 
15
15
  #### How it works
16
16
 
17
- A guard is a subclass of
18
- [`LLM::Guard`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard.html)
17
+ An intercept is a subclass of
18
+ [`LLM::Intercept`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html)
19
19
  that implements
20
- [`LLM::Guard#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard.html#call-instance_method).
21
- The guard is stamped onto the functions the context binds, and when a
22
- function's task runs the guard is checked on the calling thread before
23
- the tool is handed to the strategy. `call` receives the pending
20
+ [`LLM::Intercept#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html#call-instance_method).
21
+ It is stamped onto the functions the context binds, and when a
22
+ function's task runs the intercept is checked on the calling thread
23
+ before the tool is handed to the strategy. `call` receives the pending
24
24
  `function:` and returns an
25
25
  [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html)
26
- to close that single call, or `nil` to let it run. The guard inspects
27
- the current conversation through the `messages` helper and reads the
28
- pending call through the function's own accessors.
26
+ to close that single call, or `nil` to let it run. The intercept
27
+ inspects the current conversation through the `messages` helper and
28
+ reads the pending call through the function's own accessors.
29
29
 
30
- Configure a guard by passing a class through the `guard:` option;
31
- options given in `guard_options:` are forwarded to `call` as keyword
32
- arguments:
30
+ Configure an intercept by passing a class through the `intercept:`
31
+ option; options given in `intercept_options:` are forwarded to `call`
32
+ as keyword arguments:
33
33
 
34
34
  ```ruby
35
- class RateLimitGuard < LLM::Guard
35
+ class RateLimitIntercept < LLM::Intercept
36
36
  def call(function:, limit: 10)
37
37
  if messages.count(&:tool_return?) >= limit
38
- function.return(error: true, type: "guard_error",
38
+ function.return(error: true, type: "intercept_error",
39
39
  message: "too many tool calls")
40
40
  end
41
41
  end
@@ -43,73 +43,77 @@ end
43
43
 
44
44
  agent = LLM::Agent.new(
45
45
  llm,
46
- guard: RateLimitGuard,
47
- guard_options: {limit: 3}
46
+ intercept: RateLimitIntercept,
47
+ intercept_options: {limit: 3}
48
48
  )
49
49
  agent.talk "Research the market", tools: [FetchNews, FetchStocks]
50
50
  ```
51
51
 
52
52
  #### Why would I use it?
53
53
 
54
- A guard is the one hook that sees every pending tool call and the
55
- full conversation at once. It can observe what the model is about
56
- to do, cancel a call that needs approval, block one that violates
57
- policy, answer a cheap question without running a tool, or stop
58
- work when a budget is spent. Because the guard runs before the
59
- tool, anything it intercepts never executes.
54
+ An intercept is the one hook that sees every pending tool call and the
55
+ full conversation at once. It can observe what the model is about to
56
+ do, cancel a call that needs approval, block one that violates policy,
57
+ answer a cheap question without running a tool, or stop work when a
58
+ budget is spent. Because the intercept runs before the tool, anything
59
+ it intercepts never executes.
60
60
 
61
61
  #### Notes
62
62
 
63
- [`LLM::Guard::Null`](https://r.uby.dev/api-docs/llm.rb/LLM/Guard/Null.html)
64
- is the default guard for
63
+ [`LLM::Intercept::Null`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept/Null.html)
64
+ is the default intercept for
65
65
  [`LLM::Context`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html);
66
66
  it never blocks tool work.
67
- [`LLM::Context#guard`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#guard)
68
- returns the configured guard class. Because `call` returns an
67
+ [`LLM::Context#intercept`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#intercept-instance_method)
68
+ returns the configured intercept. Because `call` returns an
69
69
  [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html)
70
- rather than a warning string, a guard can block an individual tool
71
- call while the rest of the batch still executes. The guard is stamped
72
- onto every function the context binds, so it runs whenever a task is
73
- spawned, including tool calls a stream queues itself during a
74
- streaming turn. A blocked call yields its return without executing.
75
-
76
- The runtime creates a guard instance for each batch of tool calls and
77
- stamps it onto the functions in that batch, so the same instance sees
78
- every call in a batch. State in an instance variable therefore lasts
79
- for the batch, but not from one batch to the next. Anything a guard
80
- needs to remember across batches, like how many calls already ran,
81
- must come from the conversation (`messages`) or from class-level
82
- state.
70
+ rather than a warning string, an intercept can block an individual tool
71
+ call while the rest of the batch still executes. It is stamped onto
72
+ every function the context binds, so it runs whenever a task is
73
+ spawned, including tool calls a stream queues itself during a streaming
74
+ turn. A blocked call yields its return without executing.
75
+
76
+ The runtime builds an intercept instance for each turn and stamps it
77
+ onto the functions of that turn, so the same instance sees every call
78
+ in the turn. State in an instance variable therefore lasts for the
79
+ turn, but not from one turn to the next. Anything an intercept needs to
80
+ remember across turns, like how many calls already ran, must come from
81
+ the conversation (`messages`) or from class-level state.
82
+
83
+ `intercept:` also takes an instance rather than a class, which is how
84
+ [`LLM::Intercept.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html#multi-class_method)
85
+ is attached: it asks each intercept it holds in turn and stops at the
86
+ first one that answers the call.
83
87
 
84
88
  ### Inspect
85
89
 
86
90
  #### Overview
87
91
 
88
- The guard receives the pending
92
+ The intercept receives the pending
89
93
  [`LLM::Function`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
90
94
  and can read what the model is about to do before anything runs.
91
95
  The function exposes its `name`, `arguments`, and parameter schema,
92
- so a guard can base its decision on the actual call rather than on
93
- the whole conversation. The guard also holds the context, so the
96
+ so an intercept can base its decision on the actual call rather than on
97
+ the whole conversation. The intercept also holds the context, so the
94
98
  message history, token usage, and cost are visible too.
95
99
 
96
100
  #### How it works
97
101
 
98
102
  When you want to inspect a pending call before it runs, read the
99
103
  function's accessors inside `call`.
100
- [`LLM::Function#name`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#name)
104
+ [`LLM::Function#name`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#name-instance_method)
101
105
  is the tool name, and
102
- [`LLM::Function#arguments`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#arguments)
106
+ [`LLM::Function#arguments`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#arguments-instance_method)
103
107
  is an
104
108
  [`LLM::Object`](https://r.uby.dev/api-docs/llm.rb/LLM/Object.html)
105
109
  with the parsed arguments. The full parameter schema is available
106
110
  through
107
- [`LLM::Function#params`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#params).
108
- Returning `nil` lets the call run, so an inspection-only guard is a
111
+ [`LLM::Function#params`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#params-instance_method).
112
+ Returning `nil` lets the call run, so an inspection-only intercept is a
109
113
  pure observer:
110
114
 
111
115
  ```ruby
112
- class AuditGuard < LLM::Guard
116
+ class AuditIntercept < LLM::Intercept
113
117
  def call(function:)
114
118
  warn "pending: #{function.name}(#{function.arguments.inspect})"
115
119
  nil # let it run
@@ -120,14 +124,14 @@ end
120
124
  #### Why would I use it?
121
125
 
122
126
  Inspecting the pending call is the basis for policy, validation,
123
- and audit. A guard can log every call into a telemetry stream, or
127
+ and audit. An intercept can log every call into a telemetry stream, or
124
128
  reject a single call because its arguments violate a rule while
125
129
  letting every other call through. The same read underpins the
126
130
  richer decisions in the rest of this document.
127
131
 
128
132
  #### Notes
129
133
 
130
- The guard sees the parsed arguments exactly as the model requested
134
+ The intercept sees the parsed arguments exactly as the model requested
131
135
  them. Reading them costs nothing and never executes the tool.
132
136
  Returning `nil` means the call proceeds normally.
133
137
 
@@ -136,7 +140,7 @@ Returning `nil` means the call proceeds normally.
136
140
  #### Overview
137
141
 
138
142
  Cancelling is distinct from blocking with an error.
139
- [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel)
143
+ [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method)
140
144
  produces a return the model reads as a declined call, not a failed
141
145
  one. The conversation stays honest: the model asked for something,
142
146
  and the runtime declined it with a reason.
@@ -144,14 +148,14 @@ and the runtime declined it with a reason.
144
148
  #### How it works
145
149
 
146
150
  When you want to cancel a single pending call, call the
147
- [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel)
151
+ [`LLM::Function#cancel`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html#cancel-instance_method)
148
152
  method with a `reason:` and return the result from `call`. The
149
153
  pending call never executes, and the rest of the batch still runs.
150
- A guard can cancel one call in a two-call batch and the other call
151
- still executes:
154
+ An intercept can cancel one call in a two-call batch and the other
155
+ call still executes:
152
156
 
153
157
  ```ruby
154
- class ApprovalGuard < LLM::Guard
158
+ class ApprovalIntercept < LLM::Intercept
155
159
  def call(function:)
156
160
  if function.name == "delete-file"
157
161
  function.cancel(reason: "delete requires human approval")
@@ -183,8 +187,8 @@ reason string is what the model sees, so write it as guidance
183
187
 
184
188
  #### Overview
185
189
 
186
- Blocking is how a guard enforces policy. A blocked call never runs,
187
- and the model receives an in-band error explaining why. Unlike a
190
+ Blocking is how an intercept enforces policy. A blocked call never
191
+ runs, and the model receives an in-band error explaining why. Unlike a
188
192
  cancellation, an error tells the model the tool could not produce
189
193
  a result at all.
190
194
 
@@ -197,7 +201,7 @@ model sees them inside the return value. Returning `nil` lets the
197
201
  call through:
198
202
 
199
203
  ```ruby
200
- class PolicyGuard < LLM::Guard
204
+ class PolicyIntercept < LLM::Intercept
201
205
  def call(function:)
202
206
  if function.name == "exec"
203
207
  function.return(error: true, type: "policy_error",
@@ -223,8 +227,8 @@ call was blocked and can change course.
223
227
 
224
228
  #### Overview
225
229
 
226
- A guard's return is injected into the conversation as if the tool
227
- had executed, so a guard can answer for a tool that never runs.
230
+ An intercept's return is injected into the conversation as if the tool
231
+ had executed, so an intercept can answer for a tool that never runs.
228
232
  From the model's side, a synthesized result is indistinguishable
229
233
  from a real one.
230
234
 
@@ -237,7 +241,7 @@ helper. The value you pass becomes the tool's result as-is, so it
237
241
  must look like a plausible answer:
238
242
 
239
243
  ```ruby
240
- class CacheGuard < LLM::Guard
244
+ class CacheIntercept < LLM::Intercept
241
245
  CACHE = {"get-weather:tokyo" => {forecast: "sunny"}}
242
246
 
243
247
  def call(function:)
@@ -260,7 +264,7 @@ given environment.
260
264
  marks a tool as not found, and
261
265
  [`LLM::Function#budget_spent`](https://r.uby.dev/api-docs/llm.rb/LLM/Function.html)
262
266
  reports that the tool budget is exhausted. Both are returns too,
263
- so they flow back to the model like any tool result. A guard that
267
+ so they flow back to the model like any tool result. An intercept that
264
268
  synthesizes a result runs instead of the tool, so side effects the
265
269
  tool would have performed, like a database write, are skipped.
266
270
 
@@ -268,25 +272,25 @@ tool would have performed, like a database write, are skipped.
268
272
 
269
273
  #### Overview
270
274
 
271
- A guard can read the accumulated usage and cost of the conversation
272
- through the context. That makes it the natural place to enforce a
273
- hard ceiling: stop calling tools once a cost or token budget is
274
- spent, or once the context window is nearly full.
275
+ An intercept can read the accumulated usage and cost of the
276
+ conversation through the context. That makes it the natural place to
277
+ enforce a hard ceiling: stop calling tools once a cost or token budget
278
+ is spent, or once the context window is nearly full.
275
279
 
276
280
  #### How it works
277
281
 
278
282
  When you want to enforce a budget, compare
279
- [`LLM::Context#usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#usage)
283
+ [`LLM::Context#usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#usage-instance_method)
280
284
  or
281
285
  [`LLM::Context#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#cost-instance_method)
282
286
  against a limit inside `call`. Pass the limit through
283
- `guard_options:` so it can be tuned per context. Returning a
287
+ `intercept_options:` so it can be tuned per context. Returning a
284
288
  cancellation or an error closes the call before it runs:
285
289
 
286
290
  ```ruby
287
- class BudgetGuard < LLM::Guard
291
+ class BudgetIntercept < LLM::Intercept
288
292
  def call(function:, limit: 0.05)
289
- if ctx.cost.total >= limit
293
+ if context.cost.total >= limit
290
294
  function.cancel(reason: "cost ceiling reached, ask before continuing")
291
295
  end
292
296
  end
@@ -294,8 +298,8 @@ end
294
298
 
295
299
  ctx = LLM::Context.new(
296
300
  llm,
297
- guard: BudgetGuard,
298
- guard_options: {limit: 0.01}
301
+ intercept: BudgetIntercept,
302
+ intercept_options: {limit: 0.01}
299
303
  )
300
304
  ```
301
305
 
@@ -306,9 +310,9 @@ many tools to call. A cost ceiling keeps a runaway agent from
306
310
  spending money, and a quota derived from `messages` (as in the
307
311
  rate-limit example above) keeps a single user from exhausting
308
312
  shared resources. The same comparison works with
309
- [`LLM::Context#usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#usage)
313
+ [`LLM::Context#usage`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#usage-instance_method)
310
314
  against
311
- [`LLM::Context#context_window`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#context_window)
315
+ [`LLM::Context#context_window`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#context_window-instance_method)
312
316
  to keep the conversation inside the model's window.
313
317
 
314
318
  #### Notes
@@ -316,8 +320,8 @@ to keep the conversation inside the model's window.
316
320
  The cost reported by
317
321
  [`LLM::Context#cost`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#cost-instance_method)
318
322
  reflects the conversation so far, so a limit is enforced per call:
319
- once the ceiling is crossed, the next pending call is declined. The
320
- guard is not a replacement for
323
+ once the ceiling is crossed, the next pending call is declined. An
324
+ intercept is not a replacement for
321
325
  [`LLM::Agent.tool_budget`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#tool_budget-class_method),
322
326
  which caps the number of tool calls in a single turn. The two
323
- compose: the budget caps call count, and a guard enforces cost.
327
+ compose: the budget caps call count, and an intercept enforces cost.