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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +510 -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 +244 -269
- 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/lib/llm/agent.rb
CHANGED
|
@@ -2,48 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
|
-
# {LLM::
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# and instructions.
|
|
5
|
+
# Unlike {LLM::Context LLM::Context}
|
|
6
|
+
# the {LLM::Agent LLM::Agent} class
|
|
7
|
+
# automatically manages the tool loop.
|
|
9
8
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
9
|
+
# @example class Agent < LLM::Agent
|
|
10
|
+
# class Agent < LLM::Agent
|
|
11
|
+
# set name: "sam altman",
|
|
12
|
+
# description: "a ceo",
|
|
13
|
+
# instructions: "beat the competition",
|
|
14
|
+
# tools: :toolset,
|
|
15
|
+
# stream: proc { Stream.new(llm) }
|
|
16
16
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# next request goes out.
|
|
22
|
-
# * An agent automatically executes tool loops (unlike {LLM::Context LLM::Context}).
|
|
23
|
-
# * The tool loop can be bounded with `tool_budget`. Once the budget is
|
|
24
|
-
# spent, no further tool calls are run for that turn: the agent sends an
|
|
25
|
-
# in-band advisory message back through the model instead, and keeps
|
|
26
|
-
# sending it while the model keeps asking for tools. The budget counts
|
|
27
|
-
# tool calls, so a batch of calls is spent as a batch, and a batch that
|
|
28
|
-
# would take the turn past its budget is not run at all. By default no
|
|
29
|
-
# budget is set (`nil`), so the feature is disabled.
|
|
30
|
-
# * Tool loop execution can be configured with `concurrency :sequential`,
|
|
31
|
-
# `:thread`, `:async`, `:fiber`, `:fork`, or `:ractor`.
|
|
32
|
-
#
|
|
33
|
-
# @example Subclass with defaults
|
|
34
|
-
# class SystemAdmin < LLM::Agent
|
|
35
|
-
# set model: "gpt-4.1-nano",
|
|
36
|
-
# instructions: "You are a Linux system admin",
|
|
37
|
-
# tools: [LLM::Tool::Exec],
|
|
38
|
-
# schema: Result
|
|
17
|
+
# private
|
|
18
|
+
# def toolset
|
|
19
|
+
# [LLM::Tool::Exec]
|
|
20
|
+
# end
|
|
39
21
|
# end
|
|
40
22
|
#
|
|
41
|
-
# llm
|
|
42
|
-
# agent =
|
|
43
|
-
# agent.talk("
|
|
23
|
+
# llm = LLM.openai
|
|
24
|
+
# agent = Agent.new(llm)
|
|
25
|
+
# agent.talk("setup ollama to run gpt-oss")
|
|
44
26
|
#
|
|
45
|
-
# @example
|
|
46
|
-
# llm
|
|
27
|
+
# @example LLM::Agent.new
|
|
28
|
+
# llm = LLM.deepseek
|
|
47
29
|
# agent = LLM::Agent.new(llm, stream: $stdout)
|
|
48
30
|
# agent.talk "Hello world"
|
|
49
31
|
#
|
|
@@ -52,6 +34,7 @@ module LLM
|
|
|
52
34
|
# @see LLM::Stream Stream callbacks for model output
|
|
53
35
|
class Agent
|
|
54
36
|
require_relative "agent/interrupt"
|
|
37
|
+
require_relative "agent/registry"
|
|
55
38
|
|
|
56
39
|
##
|
|
57
40
|
# @api private
|
|
@@ -59,15 +42,12 @@ module LLM
|
|
|
59
42
|
private_constant :UNDEFINED
|
|
60
43
|
|
|
61
44
|
##
|
|
62
|
-
#
|
|
63
|
-
# turn can tell it from a message the caller composed. The role
|
|
64
|
-
# cannot do that: it is true of whatever the caller put there.
|
|
45
|
+
# @return [Hash]
|
|
65
46
|
# @api private
|
|
66
47
|
INSTRUCTIONS = {instructions: true}.freeze
|
|
67
48
|
private_constant :INSTRUCTIONS
|
|
68
49
|
|
|
69
50
|
##
|
|
70
|
-
# Ugh :)
|
|
71
51
|
# @api private
|
|
72
52
|
FIELDS = %i[record
|
|
73
53
|
name description
|
|
@@ -76,12 +56,14 @@ module LLM
|
|
|
76
56
|
skills schema
|
|
77
57
|
tracer stream
|
|
78
58
|
tools concurrency
|
|
79
|
-
instructions confirm
|
|
59
|
+
instructions confirm
|
|
60
|
+
intercept]
|
|
80
61
|
IVARS = %i[record
|
|
81
62
|
name description
|
|
82
63
|
path tool_budget
|
|
83
64
|
tracer concurrency
|
|
84
|
-
instructions confirm
|
|
65
|
+
instructions confirm
|
|
66
|
+
intercept]
|
|
85
67
|
private_constant :FIELDS, :IVARS
|
|
86
68
|
|
|
87
69
|
##
|
|
@@ -94,39 +76,54 @@ module LLM
|
|
|
94
76
|
File = ::File
|
|
95
77
|
private_constant :File
|
|
96
78
|
|
|
79
|
+
##
|
|
80
|
+
# @return [LLM::Agent::Registry]
|
|
81
|
+
REGISTRY = Registry.new
|
|
82
|
+
private_constant :REGISTRY
|
|
83
|
+
|
|
97
84
|
##
|
|
98
85
|
# Returns a provider
|
|
99
86
|
# @return [LLM::Provider]
|
|
100
87
|
attr_reader :llm
|
|
101
88
|
|
|
102
89
|
##
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
90
|
+
# Returns an instance of {LLM::Agent::Registry LLM::Agent::Registry}.
|
|
91
|
+
# @note
|
|
92
|
+
# The registry holds all _active_
|
|
93
|
+
# instances of {LLM::Agent LLM::Agent}
|
|
94
|
+
# and inactive agents are automatically
|
|
95
|
+
# removed from the registry. The registry
|
|
96
|
+
# is local to a process, and its children.
|
|
97
|
+
# @return [LLM::Agent::Registry]
|
|
98
|
+
def self.registry
|
|
99
|
+
REGISTRY
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
##
|
|
103
|
+
# Bulk-assign class-level defaults
|
|
108
104
|
#
|
|
109
105
|
# @example
|
|
110
|
-
# class
|
|
111
|
-
# set name: "
|
|
112
|
-
#
|
|
113
|
-
#
|
|
106
|
+
# class Agent < LLM::Agent
|
|
107
|
+
# set name: "007",
|
|
108
|
+
# description: "a secret agrent",
|
|
109
|
+
# instructions: "find top secret material",
|
|
114
110
|
# tools: [LLM::Tool::Exec, LLM::Tool::ReadFile]
|
|
115
111
|
# end
|
|
116
112
|
#
|
|
117
113
|
# @param [Hash] properties
|
|
118
114
|
# @option properties [String, Symbol, Proc] :name
|
|
119
115
|
# @option properties [String, Symbol, Proc] :description
|
|
120
|
-
# @option properties [String] :instructions
|
|
121
|
-
# @option properties [String] :model
|
|
122
|
-
# @option properties [
|
|
123
|
-
# @option properties [
|
|
124
|
-
# @option properties [
|
|
116
|
+
# @option properties [String, Symbol, Proc] :instructions
|
|
117
|
+
# @option properties [String, Symbol, Proc] :model
|
|
118
|
+
# @option properties [LLM::Tracer, Symbol, Proc] :tracer
|
|
119
|
+
# @option properties [LLM::Stream, Symbol, Proc] :stream
|
|
120
|
+
# @option properties [LLM::Schema, Symbol, Proc] :schema
|
|
121
|
+
# @option properties [Array<LLM::Function>, Symbol, Proc] :tools
|
|
122
|
+
# @option properties [Array<String>, Symbol, Proc] :skills
|
|
125
123
|
# @option properties [Symbol, Array<Symbol>] :concurrency
|
|
126
|
-
# @option properties [LLM::Tracer, Proc] :tracer
|
|
127
|
-
# @option properties [Object, Proc] :stream
|
|
128
124
|
# @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
|
|
129
|
-
# @raise [KeyError]
|
|
125
|
+
# @raise [KeyError]
|
|
126
|
+
# When a given key does not match a class-level accessor
|
|
130
127
|
# @return [void]
|
|
131
128
|
def self.set(properties)
|
|
132
129
|
properties.each do
|
|
@@ -189,162 +186,121 @@ module LLM
|
|
|
189
186
|
end
|
|
190
187
|
|
|
191
188
|
##
|
|
192
|
-
# Set or get the
|
|
193
|
-
# @param [
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# The schema
|
|
206
|
-
# @return [#to_json, nil]
|
|
207
|
-
# Returns the current schema when no argument is provided
|
|
208
|
-
def self.schema(schema = nil, &block)
|
|
209
|
-
return @schema if schema.nil? and !block
|
|
210
|
-
@schema = block || schema
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
##
|
|
214
|
-
# Set or get the default tools
|
|
215
|
-
# @param [Array<LLM::Function>, nil] tools
|
|
216
|
-
# One or more tools
|
|
217
|
-
# @return [Array<LLM::Function>]
|
|
218
|
-
# Returns the current tools when no argument is provided
|
|
219
|
-
def self.tools(*tools, &block)
|
|
220
|
-
return @tools || [] if tools.empty? and !block
|
|
221
|
-
if block
|
|
222
|
-
@tools = block
|
|
223
|
-
elsif single_callable?(tools)
|
|
224
|
-
@tools = tools.first
|
|
189
|
+
# Set or get the tool execution concurrency.
|
|
190
|
+
# @param [Symbol, Array<Symbol>, nil] concurrency
|
|
191
|
+
# Controls how pending tool loops are executed:
|
|
192
|
+
# - `:sequential`: sequential calls
|
|
193
|
+
# - `:thread`: concurrent threads
|
|
194
|
+
# - `:async`: concurrent async tasks
|
|
195
|
+
# - `:fiber`: concurrent scheduler-backed fibers
|
|
196
|
+
# - `:fork`: a child process
|
|
197
|
+
# - `:ractor`: Ruby-based ractors
|
|
198
|
+
# @return [Symbol, Array<Symbol>, nil]
|
|
199
|
+
def self.concurrency(concurrency = UNDEFINED)
|
|
200
|
+
if concurrency.equal?(UNDEFINED)
|
|
201
|
+
@concurrency
|
|
225
202
|
else
|
|
226
|
-
@
|
|
203
|
+
@concurrency = concurrency
|
|
227
204
|
end
|
|
228
205
|
end
|
|
229
206
|
|
|
230
207
|
##
|
|
231
|
-
# Set or get the
|
|
232
|
-
# @param [
|
|
233
|
-
#
|
|
234
|
-
# @
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
@skills = skills.first
|
|
208
|
+
# Set or get the model an agent will use
|
|
209
|
+
# @param [String, Symbol] model
|
|
210
|
+
# The model
|
|
211
|
+
# @param [Proc] block
|
|
212
|
+
# A block that is lazy-evaled.
|
|
213
|
+
# @return [String, nil]
|
|
214
|
+
# Returns the model
|
|
215
|
+
def self.model(model = UNDEFINED, &block)
|
|
216
|
+
if model.equal?(UNDEFINED) and block.nil?
|
|
217
|
+
@model
|
|
242
218
|
else
|
|
243
|
-
@
|
|
219
|
+
@model = block || model
|
|
244
220
|
end
|
|
245
221
|
end
|
|
246
222
|
|
|
247
223
|
##
|
|
248
|
-
# Set or get
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
# initialization time by calling the method with that name on the agent
|
|
252
|
-
# instance. This allows dynamic tool confirmation lists.
|
|
253
|
-
#
|
|
254
|
-
# @example
|
|
255
|
-
# class MyAgent < LLM::Agent
|
|
256
|
-
# confirm :tools_that_need_confirmation
|
|
257
|
-
#
|
|
258
|
-
# def tools_that_need_confirmation
|
|
259
|
-
# some_condition ? %w[delete destroy] : %w[delete]
|
|
260
|
-
# end
|
|
261
|
-
# end
|
|
262
|
-
#
|
|
263
|
-
# @param [String, Symbol, Array<String, Symbol>, Proc] tool_names
|
|
264
|
-
# One or more tool names.
|
|
224
|
+
# Set or get an agent's schema
|
|
225
|
+
# @param [LLM::Schema, Symbol] schema
|
|
226
|
+
# The schema
|
|
265
227
|
# @param [Proc] block
|
|
266
|
-
#
|
|
267
|
-
# @return [
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
@
|
|
272
|
-
elsif single_callable?(tool_names)
|
|
273
|
-
@confirm = tool_names.first
|
|
228
|
+
# A block that is lazy-evaled.
|
|
229
|
+
# @return [LLM::Schema, nil]
|
|
230
|
+
# Returns a schema
|
|
231
|
+
def self.schema(schema = UNDEFINED, &block)
|
|
232
|
+
if schema.equal?(UNDEFINED) and block.nil?
|
|
233
|
+
@schema
|
|
274
234
|
else
|
|
275
|
-
@
|
|
235
|
+
@schema = block || schema
|
|
276
236
|
end
|
|
277
237
|
end
|
|
278
238
|
|
|
279
239
|
##
|
|
280
|
-
# Set or get
|
|
281
|
-
# @param [String, nil] instructions
|
|
240
|
+
# Set or get an agent's instructions
|
|
241
|
+
# @param [String, Symbol, nil] instructions
|
|
282
242
|
# The system instructions
|
|
243
|
+
# @param [Proc] block
|
|
244
|
+
# A block that is lazy-evaled.
|
|
283
245
|
# @return [String, nil]
|
|
284
|
-
# Returns the
|
|
285
|
-
def self.instructions(instructions =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
# Set or get the tool execution concurrency.
|
|
292
|
-
#
|
|
293
|
-
# @param [Symbol, Array<Symbol>, nil] concurrency
|
|
294
|
-
# Controls how pending tool loops are executed:
|
|
295
|
-
# - `:sequential`: sequential calls
|
|
296
|
-
# - `:thread`: concurrent threads
|
|
297
|
-
# - `:async`: concurrent async tasks
|
|
298
|
-
# - `:fiber`: concurrent scheduler-backed fibers
|
|
299
|
-
# - `:fork`: forked child processes
|
|
300
|
-
# - `:ractor`: concurrent Ruby ractors for class-based tools; MCP tools are not supported,
|
|
301
|
-
# and this mode is especially useful for CPU-bound tool work
|
|
302
|
-
# Usually pass a single strategy. Arrays are only for advanced mixed-work
|
|
303
|
-
# cases and are not needed for normal queued stream tool loops.
|
|
304
|
-
# @return [Symbol, Array<Symbol>, nil]
|
|
305
|
-
def self.concurrency(concurrency = nil)
|
|
306
|
-
return @concurrency if concurrency.nil?
|
|
307
|
-
@concurrency = concurrency
|
|
246
|
+
# Returns the instructions
|
|
247
|
+
def self.instructions(instructions = UNDEFINED, &block)
|
|
248
|
+
if instructions.equal?(UNDEFINED) and block.nil?
|
|
249
|
+
@instructions
|
|
250
|
+
else
|
|
251
|
+
@instructions = block || instructions
|
|
252
|
+
end
|
|
308
253
|
end
|
|
309
254
|
|
|
310
255
|
##
|
|
311
|
-
# Set or get
|
|
312
|
-
#
|
|
313
|
-
# When a block is provided, it is stored and evaluated lazily against the
|
|
314
|
-
# agent instance during initialization so it can build a tracer from the
|
|
315
|
-
# resolved provider.
|
|
316
|
-
#
|
|
256
|
+
# Set or get an agent's tracer.
|
|
317
257
|
# @example
|
|
318
258
|
# class Agent < LLM::Agent
|
|
319
|
-
# tracer { LLM::Tracer.logger(llm, io: $stdout) }
|
|
259
|
+
# set tracer: proc { LLM::Tracer.logger(llm, io: $stdout) }
|
|
320
260
|
# end
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
# @
|
|
261
|
+
# @param [LLM::Tracer, Symbol, nil] tracer
|
|
262
|
+
# A tracer.
|
|
263
|
+
# @param [Proc] block
|
|
264
|
+
# A block that is lazy-evaled.
|
|
324
265
|
# @return [LLM::Tracer, Proc, nil]
|
|
325
|
-
def self.tracer(tracer =
|
|
326
|
-
|
|
327
|
-
|
|
266
|
+
def self.tracer(tracer = UNDEFINED, &block)
|
|
267
|
+
if tracer.equal?(UNDEFINED) and block.nil?
|
|
268
|
+
@tracer
|
|
269
|
+
else
|
|
270
|
+
@tracer = block || tracer
|
|
271
|
+
end
|
|
328
272
|
end
|
|
329
273
|
|
|
330
274
|
##
|
|
331
|
-
# Set or get
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
275
|
+
# Set or get an agent's intercept.
|
|
276
|
+
# @param [LLM::Intercept] intercept
|
|
277
|
+
# An intercept object
|
|
278
|
+
# @param [Proc] block
|
|
279
|
+
# A block that is lazy-evaled.
|
|
280
|
+
# @return [LLM::Intercept, nil]
|
|
281
|
+
def self.intercept(intercept = UNDEFINED, &block)
|
|
282
|
+
if intercept.equal?(UNDEFINED) and block.nil?
|
|
283
|
+
@intercept
|
|
284
|
+
else
|
|
285
|
+
@intercept = block || intercept
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
##
|
|
290
|
+
# Set or get an agent's stream
|
|
337
291
|
# @example
|
|
338
292
|
# class Agent < LLM::Agent
|
|
339
|
-
# stream {
|
|
293
|
+
# set stream: proc { Stream.new }
|
|
340
294
|
# end
|
|
341
|
-
#
|
|
342
295
|
# @param [Object, Proc, nil] stream
|
|
343
296
|
# @yieldreturn [Object, nil]
|
|
344
297
|
# @return [Object, Proc, nil]
|
|
345
|
-
def self.stream(stream =
|
|
346
|
-
|
|
347
|
-
|
|
298
|
+
def self.stream(stream = UNDEFINED, &block)
|
|
299
|
+
if stream.equal?(UNDEFINED) and block.nil?
|
|
300
|
+
@stream
|
|
301
|
+
else
|
|
302
|
+
@stream = block || stream
|
|
303
|
+
end
|
|
348
304
|
end
|
|
349
305
|
|
|
350
306
|
##
|
|
@@ -363,22 +319,14 @@ module LLM
|
|
|
363
319
|
|
|
364
320
|
##
|
|
365
321
|
# Set or get the maximum number of tool calls
|
|
366
|
-
#
|
|
367
|
-
# budget is spent, no further tool calls are run:
|
|
368
|
-
# we return an in-band message that informs the
|
|
369
|
-
# model it has spent its tool call budget, and
|
|
370
|
-
# keep returning it while the model keeps asking
|
|
371
|
-
# for tools - a model will usually change course
|
|
372
|
-
# afterwards. The budget counts tool calls, so a
|
|
373
|
-
# batch of calls is spent as a batch, and a batch
|
|
374
|
-
# that would take the turn past its budget is not
|
|
375
|
-
# run at all.
|
|
322
|
+
# an agent can make in a single turn.
|
|
376
323
|
# @note
|
|
377
|
-
#
|
|
378
|
-
# (set to `nil`).
|
|
324
|
+
# This feature is disabled by default.
|
|
379
325
|
# @param [Integer] budget
|
|
380
326
|
# The maximum number of tool calls to allow in
|
|
381
327
|
# a single turn.
|
|
328
|
+
# @param [Proc] block
|
|
329
|
+
# A block that is lazy-evaled
|
|
382
330
|
# @return [Integer, nil]
|
|
383
331
|
def self.tool_budget(budget = UNDEFINED, &block)
|
|
384
332
|
if budget.equal?(UNDEFINED) and block.nil?
|
|
@@ -389,21 +337,80 @@ module LLM
|
|
|
389
337
|
end
|
|
390
338
|
|
|
391
339
|
##
|
|
392
|
-
#
|
|
393
|
-
#
|
|
394
|
-
# The retry budget is the maximum number of times a rate-limited
|
|
395
|
-
# request will be retried before giving up. Each retry sleeps a
|
|
396
|
-
# growing interval, so an exhausted budget surfaces the rate-limit
|
|
397
|
-
# error instead of blocking indefinitely. Enabled (5) by default; a
|
|
398
|
-
# raw {LLM::Context} disables it (0) unless configured.
|
|
340
|
+
# Set or get the number of attempts an agent
|
|
341
|
+
# will make when given a failed request.
|
|
399
342
|
# @param [Integer] budget
|
|
400
|
-
# The maximum number of
|
|
343
|
+
# The maximum number of retries in a turn.
|
|
344
|
+
# @param [Proc] block
|
|
345
|
+
# A block that is lazy-evaled
|
|
401
346
|
# @return [Integer, nil]
|
|
402
|
-
def self.retry_budget(budget = UNDEFINED)
|
|
403
|
-
if budget.equal?(UNDEFINED)
|
|
404
|
-
@retry_budget
|
|
347
|
+
def self.retry_budget(budget = UNDEFINED, &block)
|
|
348
|
+
if budget.equal?(UNDEFINED) and block.nil?
|
|
349
|
+
@retry_budget
|
|
350
|
+
else
|
|
351
|
+
@retry_budget = block || budget
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
##
|
|
356
|
+
# Set or get the default tools
|
|
357
|
+
# @param [Array<LLM::Function>, nil] tools
|
|
358
|
+
# One or more tools
|
|
359
|
+
# @return [Array<LLM::Function>]
|
|
360
|
+
# Returns the current tools when no argument is provided
|
|
361
|
+
def self.tools(*tools, &block)
|
|
362
|
+
return @tools || [] if tools.empty? and !block
|
|
363
|
+
if block
|
|
364
|
+
@tools = block
|
|
365
|
+
elsif single_callable?(tools)
|
|
366
|
+
@tools = tools.first
|
|
367
|
+
else
|
|
368
|
+
@tools = tools.flatten
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
##
|
|
373
|
+
# Set or get the default skills
|
|
374
|
+
# @param [Array<String>, nil] skills
|
|
375
|
+
# One or more skill directories
|
|
376
|
+
# @return [Array<String>, nil]
|
|
377
|
+
# Returns the current skills when no argument is provided
|
|
378
|
+
def self.skills(*skills, &block)
|
|
379
|
+
return @skills if skills.empty? and !block
|
|
380
|
+
if block
|
|
381
|
+
@skills = block
|
|
382
|
+
elsif single_callable?(skills)
|
|
383
|
+
@skills = skills.first
|
|
384
|
+
else
|
|
385
|
+
@skills = skills.flatten
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
##
|
|
390
|
+
# Set or get the agent tools that require confirmation.
|
|
391
|
+
#
|
|
392
|
+
# @example
|
|
393
|
+
# class MyAgent < LLM::Agent
|
|
394
|
+
# confirm :dangerous_tools
|
|
395
|
+
# private
|
|
396
|
+
# def dangerous_tools
|
|
397
|
+
# %w[delete]
|
|
398
|
+
# end
|
|
399
|
+
# end
|
|
400
|
+
#
|
|
401
|
+
# @param [String, Symbol, Array<String, Symbol>, Proc] tool_names
|
|
402
|
+
# One or more tool names.
|
|
403
|
+
# @param [Proc] block
|
|
404
|
+
# An optional, lazy-evaluated Proc
|
|
405
|
+
# @return [Array<String>, Proc, Symbol, nil]
|
|
406
|
+
def self.confirm(*tool_names, &block)
|
|
407
|
+
return @confirm if tool_names.empty? and !block
|
|
408
|
+
if block
|
|
409
|
+
@confirm = block
|
|
410
|
+
elsif single_callable?(tool_names)
|
|
411
|
+
@confirm = tool_names.first
|
|
405
412
|
else
|
|
406
|
-
@
|
|
413
|
+
@confirm = tool_names.flatten.map(&:to_s)
|
|
407
414
|
end
|
|
408
415
|
end
|
|
409
416
|
|
|
@@ -449,14 +456,9 @@ module LLM
|
|
|
449
456
|
instance_variable_set(:"@#{field}", resolved)
|
|
450
457
|
end
|
|
451
458
|
end
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
# rate limit rather than failing outright.
|
|
456
|
-
params[:retry_budget] = llm.retry_budget if params[:retry_budget].equal?(UNDEFINED)
|
|
457
|
-
##
|
|
458
|
-
# The context is bound to the same record the agent holds, so code
|
|
459
|
-
# written against a context alone can find it through `ctx.record`.
|
|
459
|
+
if params[:retry_budget].nil?
|
|
460
|
+
params[:retry_budget] = llm.retry_budget
|
|
461
|
+
end
|
|
460
462
|
@ctx = LLM::Context.new(llm, params.merge(record: @record).compact)
|
|
461
463
|
@path and File.readable?(@path) ? @ctx.restore(path:) : nil
|
|
462
464
|
end
|
|
@@ -707,17 +709,14 @@ module LLM
|
|
|
707
709
|
end
|
|
708
710
|
|
|
709
711
|
##
|
|
710
|
-
# Start an agent console
|
|
711
|
-
# with the agent and its current state. This
|
|
712
|
-
# method requires the following gems to be
|
|
713
|
-
# installed and available to require:
|
|
714
|
-
# 'unicode-display_width', 'kramdown', 'curses',
|
|
715
|
-
# 'test-cmd.rb', and 'xchan.rb'.
|
|
712
|
+
# Start an agent console.
|
|
716
713
|
#
|
|
717
714
|
# @note
|
|
718
|
-
#
|
|
719
|
-
#
|
|
720
|
-
#
|
|
715
|
+
# This method requires the following gems
|
|
716
|
+
# to be installed and available to require:
|
|
717
|
+
# 'unicode-display_width', 'kramdown',
|
|
718
|
+
# 'curses', 'test-cmd.rb', and 'xchan.rb'.
|
|
719
|
+
#
|
|
721
720
|
# @param [String] name
|
|
722
721
|
# The agent's name.
|
|
723
722
|
# Defaults to {LLM::Agent#name}.
|
|
@@ -732,11 +731,7 @@ module LLM
|
|
|
732
731
|
# When true, the tracer is kept alive during the
|
|
733
732
|
# console session. Default is false.
|
|
734
733
|
# @return [void]
|
|
735
|
-
def console(name: self.name, path: nil, tools: [], skills: [], tracer: false
|
|
736
|
-
if trace != nil
|
|
737
|
-
warn "llm.rb: trace option is deprecated, use tracer instead"
|
|
738
|
-
tracer = trace
|
|
739
|
-
end
|
|
734
|
+
def console(name: self.name, path: nil, tools: [], skills: [], tracer: false)
|
|
740
735
|
if !tracer
|
|
741
736
|
previous = self.tracer
|
|
742
737
|
self.tracer = nil
|
|
@@ -849,7 +844,7 @@ module LLM
|
|
|
849
844
|
#
|
|
850
845
|
# An agent with no instructions returns first. The caller reaches
|
|
851
846
|
# this through `apply_instructions`, which already returns early for
|
|
852
|
-
# the same reason, and the
|
|
847
|
+
# the same reason, and the check is repeated here so that the method
|
|
853
848
|
# cannot replace a message with nil however it is reached.
|
|
854
849
|
#
|
|
855
850
|
# Comparing content rather than the message is deliberate.
|
|
@@ -949,46 +944,26 @@ module LLM
|
|
|
949
944
|
end
|
|
950
945
|
res
|
|
951
946
|
end
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
# for the same reason. The tracer is part of it too, because the
|
|
973
|
-
# caller announces the turn's last phase to it before raising.
|
|
974
|
-
#
|
|
975
|
-
# Set to nil below once the turn is over, and that is not a
|
|
976
|
-
# formality: a worker's thread is reused for the turn after this
|
|
977
|
-
# one, and a cancel that arrived late would otherwise land in
|
|
978
|
-
# whatever that thread is doing.
|
|
979
|
-
@ctx.instance_variable_set(
|
|
980
|
-
:@caller,
|
|
981
|
-
LLM::Object.from(
|
|
982
|
-
thread: Thread.current,
|
|
983
|
-
fiber: @llm.request_owner,
|
|
984
|
-
scheduler: Fiber.scheduler,
|
|
985
|
-
tracer:
|
|
986
|
-
).extend(Interrupt)
|
|
987
|
-
)
|
|
988
|
-
@llm.with_tracer(tracer, &run)
|
|
989
|
-
ensure
|
|
990
|
-
@ctx.instance_variable_set(:@caller, nil)
|
|
991
|
-
tracer&.stop_trace
|
|
947
|
+
|
|
948
|
+
begin
|
|
949
|
+
tracer = @tracer || @llm.tracer
|
|
950
|
+
caller = LLM::Object.from(thread: Thread.current,
|
|
951
|
+
fiber: @llm.request_owner,
|
|
952
|
+
scheduler: Fiber.scheduler).extend(Interrupt)
|
|
953
|
+
@ctx.instance_variable_set(:@caller, caller)
|
|
954
|
+
tracer.start_trace(name: "llm.turn", trace_group_id: SecureRandom.uuid_v7)
|
|
955
|
+
REGISTRY.enter(self)
|
|
956
|
+
@llm.with_tracer(tracer) do
|
|
957
|
+
run.call
|
|
958
|
+
rescue LLM::Interrupt
|
|
959
|
+
tracer&.on_interrupt(scope: :agent)
|
|
960
|
+
raise
|
|
961
|
+
end
|
|
962
|
+
ensure
|
|
963
|
+
REGISTRY.exit(self)
|
|
964
|
+
@ctx.instance_variable_set(:@caller, nil)
|
|
965
|
+
tracer&.stop_trace
|
|
966
|
+
end
|
|
992
967
|
end
|
|
993
968
|
|
|
994
969
|
##
|