llm.rb 16.0.0 → 17.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +535 -2
  3. data/README.md +857 -501
  4. data/data/alibaba.json +2 -2
  5. data/data/anthropic.json +66 -3
  6. data/data/bedrock.json +781 -12
  7. data/data/deepinfra.json +157 -1
  8. data/data/google.json +48 -0
  9. data/data/mistral.json +326 -16
  10. data/data/moonshot.json +2 -1
  11. data/data/openai.json +13 -0
  12. data/data/openrouter.json +1078 -1235
  13. data/data/xai.json +4 -2
  14. data/docs/deepdive/advanced/cancellation.md +35 -7
  15. data/docs/deepdive/advanced/compaction.md +5 -5
  16. data/docs/deepdive/advanced/context.md +45 -24
  17. data/docs/deepdive/advanced/{guard.md → intercept.md} +87 -83
  18. data/docs/deepdive/features/builtin_tools.md +1 -1
  19. data/docs/deepdive/features/concurrency.md +15 -10
  20. data/docs/deepdive/features/database.md +19 -19
  21. data/docs/deepdive/features/embeddings.md +4 -4
  22. data/docs/deepdive/features/files.md +7 -5
  23. data/docs/deepdive/fundamentals/agents.md +13 -8
  24. data/docs/deepdive/fundamentals/providers.md +1 -1
  25. data/docs/deepdive/fundamentals/schema.md +2 -2
  26. data/docs/deepdive/fundamentals/stream.md +8 -8
  27. data/docs/deepdive/fundamentals/tools.md +14 -14
  28. data/docs/deepdive/media/ocr.md +1 -1
  29. data/docs/deepdive/protocols/a2a.md +3 -3
  30. data/docs/deepdive/protocols/mcp.md +116 -7
  31. data/docs/deepdive/reference/cost.md +1 -1
  32. data/docs/deepdive/reference/response.md +1 -1
  33. data/docs/deepdive/reference/tracer.md +90 -22
  34. data/docs/deepdive.md +1 -1
  35. data/lib/llm/agent/interrupt.rb +35 -46
  36. data/lib/llm/agent/registry.rb +106 -0
  37. data/lib/llm/agent.rb +242 -268
  38. data/lib/llm/context.rb +63 -55
  39. data/lib/llm/function/array.rb +2 -1
  40. data/lib/llm/function/async/group.rb +2 -2
  41. data/lib/llm/function/async/task.rb +25 -48
  42. data/lib/llm/function/fiber/group.rb +2 -2
  43. data/lib/llm/function/fiber/task.rb +16 -30
  44. data/lib/llm/function/fork/group.rb +2 -2
  45. data/lib/llm/function/fork/job.rb +8 -4
  46. data/lib/llm/function/fork/task.rb +25 -30
  47. data/lib/llm/function/group.rb +33 -5
  48. data/lib/llm/function/ractor/group.rb +2 -2
  49. data/lib/llm/function/ractor/job.rb +8 -6
  50. data/lib/llm/function/ractor/task.rb +32 -9
  51. data/lib/llm/function/sequential/group.rb +2 -1
  52. data/lib/llm/function/sequential/task.rb +8 -1
  53. data/lib/llm/function/task.rb +52 -4
  54. data/lib/llm/function/thread/group.rb +2 -2
  55. data/lib/llm/function/thread/task.rb +36 -10
  56. data/lib/llm/function/window.rb +16 -0
  57. data/lib/llm/function.rb +14 -43
  58. data/lib/llm/intercept/multi.rb +40 -0
  59. data/lib/llm/intercept/null.rb +18 -0
  60. data/lib/llm/intercept.rb +96 -0
  61. data/lib/llm/message.rb +1 -1
  62. data/lib/llm/provider.rb +16 -1
  63. data/lib/llm/providers/anthropic/stream_parser.rb +1 -1
  64. data/lib/llm/providers/bedrock/stream_parser.rb +1 -1
  65. data/lib/llm/providers/deepseek/files.rb +49 -0
  66. data/lib/llm/providers/deepseek/request_adapter/completion.rb +15 -13
  67. data/lib/llm/providers/deepseek.rb +16 -2
  68. data/lib/llm/providers/google/stream_parser.rb +1 -1
  69. data/lib/llm/providers/openai/responses/stream_parser.rb +1 -1
  70. data/lib/llm/providers/openai/stream_parser.rb +1 -1
  71. data/lib/llm/stream.rb +8 -7
  72. data/lib/llm/tool.rb +1 -1
  73. data/lib/llm/tools/utils.rb +1 -1
  74. data/lib/llm/tracer/logger.rb +42 -1
  75. data/lib/llm/tracer/multi.rb +144 -0
  76. data/lib/llm/tracer/null.rb +20 -0
  77. data/lib/llm/tracer/pretty_logger.rb +33 -2
  78. data/lib/llm/tracer/rescue.rb +110 -0
  79. data/lib/llm/tracer/telemetry.rb +69 -4
  80. data/lib/llm/tracer.rb +125 -48
  81. data/lib/llm/utils.rb +12 -2
  82. data/lib/llm/version.rb +1 -1
  83. data/lib/llm.rb +36 -1
  84. data/llm.gemspec +9 -4
  85. metadata +16 -35
  86. data/lib/llm/function/tracing.rb +0 -21
  87. data/lib/llm/guard/null.rb +0 -19
  88. data/lib/llm/guard.rb +0 -55
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,7 +56,8 @@ 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
@@ -94,39 +75,54 @@ module LLM
94
75
  File = ::File
95
76
  private_constant :File
96
77
 
78
+ ##
79
+ # @return [LLM::Agent::Registry]
80
+ REGISTRY = Registry.new
81
+ private_constant :REGISTRY
82
+
97
83
  ##
98
84
  # Returns a provider
99
85
  # @return [LLM::Provider]
100
86
  attr_reader :llm
101
87
 
102
88
  ##
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.
89
+ # Returns an instance of {LLM::Agent::Registry LLM::Agent::Registry}.
90
+ # @note
91
+ # The registry holds all _active_
92
+ # instances of {LLM::Agent LLM::Agent}
93
+ # and inactive agents are automatically
94
+ # removed from the registry. The registry
95
+ # is local to a process, and its children.
96
+ # @return [LLM::Agent::Registry]
97
+ def self.registry
98
+ REGISTRY
99
+ end
100
+
101
+ ##
102
+ # Bulk-assign class-level defaults
108
103
  #
109
104
  # @example
110
- # class AdminAgent < LLM::Agent
111
- # set name: "admin",
112
- # instructions: "You are a system administrator",
113
- # model: "gpt-4.1-nano",
105
+ # class Agent < LLM::Agent
106
+ # set name: "007",
107
+ # description: "a secret agrent",
108
+ # instructions: "find top secret material",
114
109
  # tools: [LLM::Tool::Exec, LLM::Tool::ReadFile]
115
110
  # end
116
111
  #
117
112
  # @param [Hash] properties
118
113
  # @option properties [String, Symbol, Proc] :name
119
114
  # @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
115
+ # @option properties [String, Symbol, Proc] :instructions
116
+ # @option properties [String, Symbol, Proc] :model
117
+ # @option properties [LLM::Tracer, Symbol, Proc] :tracer
118
+ # @option properties [LLM::Stream, Symbol, Proc] :stream
119
+ # @option properties [LLM::Schema, Symbol, Proc] :schema
120
+ # @option properties [Array<LLM::Function>, Symbol, Proc] :tools
121
+ # @option properties [Array<String>, Symbol, Proc] :skills
125
122
  # @option properties [Symbol, Array<Symbol>] :concurrency
126
- # @option properties [LLM::Tracer, Proc] :tracer
127
- # @option properties [Object, Proc] :stream
128
123
  # @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
129
- # @raise [KeyError] when a property key does not match a class-level accessor
124
+ # @raise [KeyError]
125
+ # When a given key does not match a class-level accessor
130
126
  # @return [void]
131
127
  def self.set(properties)
132
128
  properties.each do
@@ -189,162 +185,121 @@ module LLM
189
185
  end
190
186
 
191
187
  ##
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
188
+ # Set or get the tool execution concurrency.
189
+ # @param [Symbol, Array<Symbol>, nil] concurrency
190
+ # Controls how pending tool loops are executed:
191
+ # - `:sequential`: sequential calls
192
+ # - `:thread`: concurrent threads
193
+ # - `:async`: concurrent async tasks
194
+ # - `:fiber`: concurrent scheduler-backed fibers
195
+ # - `:fork`: a child process
196
+ # - `:ractor`: Ruby-based ractors
197
+ # @return [Symbol, Array<Symbol>, nil]
198
+ def self.concurrency(concurrency = UNDEFINED)
199
+ if concurrency.equal?(UNDEFINED)
200
+ @concurrency
225
201
  else
226
- @tools = tools.flatten
202
+ @concurrency = concurrency
227
203
  end
228
204
  end
229
205
 
230
206
  ##
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
207
+ # Set or get the model an agent will use
208
+ # @param [String, Symbol] model
209
+ # The model
210
+ # @param [Proc] block
211
+ # A block that is lazy-evaled.
212
+ # @return [String, nil]
213
+ # Returns the model
214
+ def self.model(model = UNDEFINED, &block)
215
+ if model.equal?(UNDEFINED) and block.nil?
216
+ @model
242
217
  else
243
- @skills = skills.flatten
218
+ @model = block || model
244
219
  end
245
220
  end
246
221
 
247
222
  ##
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.
223
+ # Set or get an agent's schema
224
+ # @param [LLM::Schema, Symbol] schema
225
+ # The schema
265
226
  # @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
227
+ # A block that is lazy-evaled.
228
+ # @return [LLM::Schema, nil]
229
+ # Returns a schema
230
+ def self.schema(schema = UNDEFINED, &block)
231
+ if schema.equal?(UNDEFINED) and block.nil?
232
+ @schema
274
233
  else
275
- @confirm = tool_names.flatten.map(&:to_s)
234
+ @schema = block || schema
276
235
  end
277
236
  end
278
237
 
279
238
  ##
280
- # Set or get the default instructions
281
- # @param [String, nil] instructions
239
+ # Set or get an agent's instructions
240
+ # @param [String, Symbol, nil] instructions
282
241
  # The system instructions
242
+ # @param [Proc] block
243
+ # A block that is lazy-evaled.
283
244
  # @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
245
+ # Returns the instructions
246
+ def self.instructions(instructions = UNDEFINED, &block)
247
+ if instructions.equal?(UNDEFINED) and block.nil?
248
+ @instructions
249
+ else
250
+ @instructions = block || instructions
251
+ end
308
252
  end
309
253
 
310
254
  ##
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
- #
255
+ # Set or get an agent's tracer.
317
256
  # @example
318
257
  # class Agent < LLM::Agent
319
- # tracer { LLM::Tracer.logger(llm, io: $stdout) }
258
+ # set tracer: proc { LLM::Tracer.logger(llm, io: $stdout) }
320
259
  # end
321
- #
322
- # @param [LLM::Tracer, Proc, nil] tracer
323
- # @yieldreturn [LLM::Tracer, nil]
260
+ # @param [LLM::Tracer, Symbol, nil] tracer
261
+ # A tracer.
262
+ # @param [Proc] block
263
+ # A block that is lazy-evaled.
324
264
  # @return [LLM::Tracer, Proc, nil]
325
- def self.tracer(tracer = nil, &block)
326
- return @tracer if tracer.nil? && !block
327
- @tracer = block || tracer
265
+ def self.tracer(tracer = UNDEFINED, &block)
266
+ if tracer.equal?(UNDEFINED) and block.nil?
267
+ @tracer
268
+ else
269
+ @tracer = block || tracer
270
+ end
328
271
  end
329
272
 
330
273
  ##
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
- #
274
+ # Set or get an agent's intercept.
275
+ # @param [LLM::Intercept] intercept
276
+ # An intercept object
277
+ # @param [Proc] block
278
+ # A block that is lazy-evaled.
279
+ # @return [LLM::Intercept, nil]
280
+ def self.intercept(intercept = UNDEFINED, &block)
281
+ if intercept.equal?(UNDEFINED) and block.nil?
282
+ @intercept
283
+ else
284
+ @intercept = block || intercept
285
+ end
286
+ end
287
+
288
+ ##
289
+ # Set or get an agent's stream
337
290
  # @example
338
291
  # class Agent < LLM::Agent
339
- # stream { MyStream.new }
292
+ # set stream: proc { Stream.new }
340
293
  # end
341
- #
342
294
  # @param [Object, Proc, nil] stream
343
295
  # @yieldreturn [Object, nil]
344
296
  # @return [Object, Proc, nil]
345
- def self.stream(stream = nil, &block)
346
- return @stream if stream.nil? && !block
347
- @stream = block || stream
297
+ def self.stream(stream = UNDEFINED, &block)
298
+ if stream.equal?(UNDEFINED) and block.nil?
299
+ @stream
300
+ else
301
+ @stream = block || stream
302
+ end
348
303
  end
349
304
 
350
305
  ##
@@ -363,22 +318,14 @@ module LLM
363
318
 
364
319
  ##
365
320
  # 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.
321
+ # an agent can make in a single turn.
376
322
  # @note
377
- # By default this feature is disabled
378
- # (set to `nil`).
323
+ # This feature is disabled by default.
379
324
  # @param [Integer] budget
380
325
  # The maximum number of tool calls to allow in
381
326
  # a single turn.
327
+ # @param [Proc] block
328
+ # A block that is lazy-evaled
382
329
  # @return [Integer, nil]
383
330
  def self.tool_budget(budget = UNDEFINED, &block)
384
331
  if budget.equal?(UNDEFINED) and block.nil?
@@ -389,21 +336,80 @@ module LLM
389
336
  end
390
337
 
391
338
  ##
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.
339
+ # Set or get the number of attempts an agent
340
+ # will make when given a failed request.
399
341
  # @param [Integer] budget
400
- # The maximum number of rate-limit retries in a turn.
342
+ # The maximum number of retries in a turn.
343
+ # @param [Proc] block
344
+ # A block that is lazy-evaled
401
345
  # @return [Integer, nil]
402
- def self.retry_budget(budget = UNDEFINED)
403
- if budget.equal?(UNDEFINED)
404
- @retry_budget.nil? ? UNDEFINED : @retry_budget
346
+ def self.retry_budget(budget = UNDEFINED, &block)
347
+ if budget.equal?(UNDEFINED) and block.nil?
348
+ @retry_budget
349
+ else
350
+ @retry_budget = block || budget
351
+ end
352
+ end
353
+
354
+ ##
355
+ # Set or get the default tools
356
+ # @param [Array<LLM::Function>, nil] tools
357
+ # One or more tools
358
+ # @return [Array<LLM::Function>]
359
+ # Returns the current tools when no argument is provided
360
+ def self.tools(*tools, &block)
361
+ return @tools || [] if tools.empty? and !block
362
+ if block
363
+ @tools = block
364
+ elsif single_callable?(tools)
365
+ @tools = tools.first
366
+ else
367
+ @tools = tools.flatten
368
+ end
369
+ end
370
+
371
+ ##
372
+ # Set or get the default skills
373
+ # @param [Array<String>, nil] skills
374
+ # One or more skill directories
375
+ # @return [Array<String>, nil]
376
+ # Returns the current skills when no argument is provided
377
+ def self.skills(*skills, &block)
378
+ return @skills if skills.empty? and !block
379
+ if block
380
+ @skills = block
381
+ elsif single_callable?(skills)
382
+ @skills = skills.first
383
+ else
384
+ @skills = skills.flatten
385
+ end
386
+ end
387
+
388
+ ##
389
+ # Set or get the agent tools that require confirmation.
390
+ #
391
+ # @example
392
+ # class MyAgent < LLM::Agent
393
+ # confirm :dangerous_tools
394
+ # private
395
+ # def dangerous_tools
396
+ # %w[delete]
397
+ # end
398
+ # end
399
+ #
400
+ # @param [String, Symbol, Array<String, Symbol>, Proc] tool_names
401
+ # One or more tool names.
402
+ # @param [Proc] block
403
+ # An optional, lazy-evaluated Proc
404
+ # @return [Array<String>, Proc, Symbol, nil]
405
+ def self.confirm(*tool_names, &block)
406
+ return @confirm if tool_names.empty? and !block
407
+ if block
408
+ @confirm = block
409
+ elsif single_callable?(tool_names)
410
+ @confirm = tool_names.first
405
411
  else
406
- @retry_budget = budget
412
+ @confirm = tool_names.flatten.map(&:to_s)
407
413
  end
408
414
  end
409
415
 
@@ -449,14 +455,9 @@ module LLM
449
455
  instance_variable_set(:"@#{field}", resolved)
450
456
  end
451
457
  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`.
458
+ if params[:retry_budget].nil?
459
+ params[:retry_budget] = llm.retry_budget
460
+ end
460
461
  @ctx = LLM::Context.new(llm, params.merge(record: @record).compact)
461
462
  @path and File.readable?(@path) ? @ctx.restore(path:) : nil
462
463
  end
@@ -707,17 +708,14 @@ module LLM
707
708
  end
708
709
 
709
710
  ##
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'.
711
+ # Start an agent console.
716
712
  #
717
713
  # @note
718
- # By default this method disables the tracer for
719
- # the duration of the console session, and restores
720
- # it afterwards.
714
+ # This method requires the following gems
715
+ # to be installed and available to require:
716
+ # 'unicode-display_width', 'kramdown',
717
+ # 'curses', 'test-cmd.rb', and 'xchan.rb'.
718
+ #
721
719
  # @param [String] name
722
720
  # The agent's name.
723
721
  # Defaults to {LLM::Agent#name}.
@@ -732,11 +730,7 @@ module LLM
732
730
  # When true, the tracer is kept alive during the
733
731
  # console session. Default is false.
734
732
  # @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
733
+ def console(name: self.name, path: nil, tools: [], skills: [], tracer: false)
740
734
  if !tracer
741
735
  previous = self.tracer
742
736
  self.tracer = nil
@@ -849,7 +843,7 @@ module LLM
849
843
  #
850
844
  # An agent with no instructions returns first. The caller reaches
851
845
  # this through `apply_instructions`, which already returns early for
852
- # the same reason, and the guard is repeated here so that the method
846
+ # the same reason, and the check is repeated here so that the method
853
847
  # cannot replace a message with nil however it is reached.
854
848
  #
855
849
  # Comparing content rather than the message is deliberate.
@@ -949,46 +943,26 @@ module LLM
949
943
  end
950
944
  res
951
945
  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
946
+
947
+ begin
948
+ tracer = @tracer || @llm.tracer
949
+ caller = LLM::Object.from(thread: Thread.current,
950
+ fiber: @llm.request_owner,
951
+ scheduler: Fiber.scheduler).extend(Interrupt)
952
+ @ctx.instance_variable_set(:@caller, caller)
953
+ tracer.start_trace(name: "llm.turn", trace_group_id: SecureRandom.uuid_v7)
954
+ REGISTRY.enter(self)
955
+ @llm.with_tracer(tracer) do
956
+ run.call
957
+ rescue LLM::Interrupt
958
+ tracer&.on_interrupt(scope: :agent)
959
+ raise
960
+ end
961
+ ensure
962
+ REGISTRY.exit(self)
963
+ @ctx.instance_variable_set(:@caller, nil)
964
+ tracer&.stop_trace
965
+ end
992
966
  end
993
967
 
994
968
  ##