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/lib/llm/agent.rb CHANGED
@@ -2,48 +2,30 @@
2
2
 
3
3
  module LLM
4
4
  ##
5
- # {LLM::Agent LLM::Agent} is the recommended entry point for most
6
- # use-cases. It provides a class-level DSL for defining reusable,
7
- # preconfigured assistants with defaults for model, tools, schema,
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
- # It wraps the same stateful runtime surface as
11
- # {LLM::Context LLM::Context}: message history, usage, persistence,
12
- # streaming parameters, and provider-backed requests still flow through
13
- # an underlying context. The defining behavior of an agent is that it
14
- # automatically resolves pending tool calls for you during `talk`,
15
- # instead of leaving tool loops to the caller.
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
- # **Notes:**
18
- # * Instructions are injected once, and kept in step after that. A
19
- # restored conversation carries the instructions that were current
20
- # when it started, and the message is brought up to date before the
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 = LLM.openai(key: ENV["KEY"])
42
- # agent = SystemAdmin.new(llm)
43
- # agent.talk("Run 'date'")
23
+ # llm = LLM.openai
24
+ # agent = Agent.new(llm)
25
+ # agent.talk("setup ollama to run gpt-oss")
44
26
  #
45
- # @example Direct instance
46
- # llm = LLM.deepseek(key: ENV["KEY"])
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
- # Marks the message an agent injects as its own, so that a later
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
- # Bulk-assign class-level agent defaults from a Hash.
104
- #
105
- # Each key is resolved by calling the corresponding class method on the
106
- # agent subclass. An error is raised for unknown keys so that typos are
107
- # caught early.
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 AdminAgent < LLM::Agent
111
- # set name: "admin",
112
- # instructions: "You are a system administrator",
113
- # model: "gpt-4.1-nano",
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 [Array<LLM::Function>] :tools
123
- # @option properties [Array<String>] :skills
124
- # @option properties [#to_json] :schema
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] when a property key does not match a class-level accessor
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 default model
193
- # @param [String, nil] model
194
- # The model identifier
195
- # @return [String, nil]
196
- # Returns the current model when no argument is provided
197
- def self.model(model = nil, &block)
198
- return @model if model.nil? and !block
199
- @model = block || model
200
- end
201
-
202
- ##
203
- # Set or get the default schema
204
- # @param [#to_json, nil] schema
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
- @tools = tools.flatten
203
+ @concurrency = concurrency
227
204
  end
228
205
  end
229
206
 
230
207
  ##
231
- # Set or get the default skills
232
- # @param [Array<String>, nil] skills
233
- # One or more skill directories
234
- # @return [Array<String>, nil]
235
- # Returns the current skills when no argument is provided
236
- def self.skills(*skills, &block)
237
- return @skills if skills.empty? and !block
238
- if block
239
- @skills = block
240
- elsif single_callable?(skills)
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
- @skills = skills.flatten
219
+ @model = block || model
244
220
  end
245
221
  end
246
222
 
247
223
  ##
248
- # Set or get the tool names that require confirmation before they can run.
249
- #
250
- # When a single Symbol is given, it is stored as-is and resolved at
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
- # An optional, lazy-evaluated Proc
267
- # @return [Array<String>, Proc, Symbol, nil]
268
- def self.confirm(*tool_names, &block)
269
- return @confirm if tool_names.empty? and !block
270
- if block
271
- @confirm = block
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
- @confirm = tool_names.flatten.map(&:to_s)
235
+ @schema = block || schema
276
236
  end
277
237
  end
278
238
 
279
239
  ##
280
- # Set or get the default instructions
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 current instructions when no argument is provided
285
- def self.instructions(instructions = nil)
286
- return @instructions if instructions.nil?
287
- @instructions = instructions
288
- end
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 the default tracer.
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
- # @param [LLM::Tracer, Proc, nil] tracer
323
- # @yieldreturn [LLM::Tracer, nil]
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 = nil, &block)
326
- return @tracer if tracer.nil? && !block
327
- @tracer = block || tracer
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 the default stream.
332
- #
333
- # When a block is provided, it is stored and evaluated lazily against the
334
- # agent instance during initialization so it can build a fresh stream for
335
- # each agent.
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 { MyStream.new }
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 = nil, &block)
346
- return @stream if stream.nil? && !block
347
- @stream = block || stream
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
- # that are allowed in a single turn. Once the
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
- # By default this feature is disabled
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
- # Sets or returns the retry budget for the agent.
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 rate-limit retries in a turn.
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.nil? ? UNDEFINED : @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
- @retry_budget = budget
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
- # The provider decides how patient a turn should be: it
454
- # knows its own API, and how often it recovers from a
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 that can interact
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
- # By default this method disables the tracer for
719
- # the duration of the console session, and restores
720
- # it afterwards.
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, trace: nil)
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 guard is repeated here so that the method
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
- # One turn, one trace group. A tracer
954
- # is told where the turn begins and
955
- # where it ends. The trace group ID
956
- # identifies the turn.
957
- tracer = @tracer || @llm.tracer
958
- tracer.start_trace(name: "llm.turn", trace_group_id: SecureRandom.uuid_v7)
959
- ##
960
- # Where the turn is running, for as long as it runs.
961
- #
962
- # A cancel reaches a request in flight and the tools that are
963
- # running, and between those it reached nothing: the loop is
964
- # between two requests, or waiting out a retry, or building the
965
- # next one. What is left over is a raise into the caller the turn
966
- # is running under, and this names it - recorded here rather than in
967
- # `Context#talk`, because a turn is a loop and not a request.
968
- #
969
- # The scheduler is part of it, and read here rather than by the
970
- # caller: a cancel runs on the canceller's thread, which is not the
971
- # thread a scheduler was installed on. The fiber strategy names it
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
  ##