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
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
|
-
|
|
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`.
|
|
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
|
-
##
|
|
2
|
+
## Intercept
|
|
3
3
|
|
|
4
4
|
### Introduction
|
|
5
5
|
|
|
6
6
|
#### Overview
|
|
7
7
|
|
|
8
|
-
[`LLM::
|
|
9
|
-
is the superclass for context-level supervisors.
|
|
10
|
-
to a context and inspects each pending tool call before it runs.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
[`LLM::
|
|
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::
|
|
21
|
-
|
|
22
|
-
function's task runs the
|
|
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
|
|
27
|
-
the current conversation through the `messages` helper and
|
|
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
|
|
31
|
-
options given in `
|
|
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
|
|
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: "
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
55
|
-
full conversation at once. It can observe what the model is about
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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::
|
|
64
|
-
is the default
|
|
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#
|
|
68
|
-
returns the configured
|
|
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,
|
|
71
|
-
call while the rest of the batch still executes.
|
|
72
|
-
|
|
73
|
-
spawned, including tool calls a stream queues itself during a
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
The runtime
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
93
|
-
the whole conversation. 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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
227
|
-
had executed, so
|
|
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
|
|
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.
|
|
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
|
-
|
|
272
|
-
through the context. That makes it the natural place to
|
|
273
|
-
hard ceiling: stop calling tools once a cost or token budget
|
|
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
|
-
`
|
|
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
|
|
291
|
+
class BudgetIntercept < LLM::Intercept
|
|
288
292
|
def call(function:, limit: 0.05)
|
|
289
|
-
if
|
|
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
|
-
|
|
298
|
-
|
|
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.
|
|
320
|
-
|
|
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
|
|
327
|
+
compose: the budget caps call count, and an intercept enforces cost.
|