llm.rb 12.1.0 → 12.3.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +248 -0
  3. data/README.md +77 -27
  4. data/data/anthropic.json +3 -2
  5. data/data/bedrock.json +47 -0
  6. data/data/deepinfra.json +2 -2
  7. data/data/google.json +32 -0
  8. data/data/mistral.json +968 -0
  9. data/data/openai.json +422 -0
  10. data/data/xai.json +60 -1
  11. data/lib/llm/active_record/acts_as_agent.rb +11 -0
  12. data/lib/llm/agent.rb +27 -3
  13. data/lib/llm/compactor.rb +2 -2
  14. data/lib/llm/context.rb +18 -26
  15. data/lib/llm/function.rb +6 -0
  16. data/lib/llm/object.rb +13 -0
  17. data/lib/llm/provider.rb +12 -9
  18. data/lib/llm/providers/anthropic.rb +4 -5
  19. data/lib/llm/providers/bedrock.rb +4 -5
  20. data/lib/llm/providers/google.rb +7 -3
  21. data/lib/llm/providers/mistral/request_adapter/completion.rb +122 -0
  22. data/lib/llm/providers/mistral/request_adapter.rb +20 -0
  23. data/lib/llm/providers/mistral.rb +145 -0
  24. data/lib/llm/providers/ollama/response_adapter/completion.rb +39 -0
  25. data/lib/llm/providers/ollama.rb +2 -3
  26. data/lib/llm/providers/openai/responses.rb +6 -3
  27. data/lib/llm/providers/openai.rb +2 -3
  28. data/lib/llm/repl/bar.rb +52 -0
  29. data/lib/llm/repl/input.rb +143 -30
  30. data/lib/llm/repl/markdown.rb +85 -0
  31. data/lib/llm/repl/status.rb +16 -5
  32. data/lib/llm/repl/stream.rb +15 -5
  33. data/lib/llm/repl/transcript.rb +107 -18
  34. data/lib/llm/repl/window.rb +43 -17
  35. data/lib/llm/repl.rb +99 -18
  36. data/lib/llm/sequel/agent.rb +11 -0
  37. data/lib/llm/skill.rb +1 -1
  38. data/lib/llm/stream/disabled.rb +23 -0
  39. data/lib/llm/stream/io.rb +43 -0
  40. data/lib/llm/stream.rb +34 -0
  41. data/lib/llm/tools/git.rb +2 -3
  42. data/lib/llm/tools/pwd.rb +0 -1
  43. data/lib/llm/tools/rg.rb +2 -1
  44. data/lib/llm/tools/swap_text.rb +6 -0
  45. data/lib/llm/transport/execution.rb +1 -0
  46. data/lib/llm/version.rb +1 -1
  47. data/lib/llm.rb +19 -0
  48. data/llm.gemspec +8 -4
  49. data/resources/deepdive.md +170 -50
  50. metadata +15 -4
data/lib/llm/repl.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  LLM.require "curses"
4
+ LLM.require "kramdown"
4
5
 
5
6
  module LLM
6
7
  ##
@@ -18,19 +19,37 @@ module LLM
18
19
  require_relative "repl/status"
19
20
  require_relative "repl/transcript"
20
21
  require_relative "repl/input"
22
+ require_relative "repl/bar"
21
23
  require_relative "repl/stream"
24
+ require_relative "repl/markdown"
22
25
 
23
26
  ##
24
27
  # @param [LLM::Agent] agent
28
+ # @param [Array<LLM::Tool>] tools
29
+ # Zero or more tools
30
+ # @param [Array<String>] skills
31
+ # Zero or more skills
25
32
  # @return [LLM::Repl]
26
- def initialize(agent)
33
+ def initialize(agent:, tools:, skills:)
27
34
  @agent = agent
28
35
  @provider = agent.llm.name
29
- @status = Status.new(@provider)
36
+ @status = Status.new(@agent)
30
37
  @transcript = Transcript.new
31
- @input = Input.new(@provider)
38
+ @input = Input.new(@agent, height: 3)
32
39
  @window = Window.new(@status, @transcript, @input)
33
- @stream = Stream.new(self)
40
+ @thread = nil
41
+ @queue = Queue.new
42
+ @stream = Stream.new(self, @queue)
43
+ @skills = skills.map do |path|
44
+ ##
45
+ # I'm not sure it would make sense to expose
46
+ # the underlying context or not. In the meantime,
47
+ # this works and meets the expectations of the
48
+ # LLM::Skill class.
49
+ ctx = agent.instance_variable_get(:@ctx)
50
+ LLM::Skill.load(path).to_tool(ctx)
51
+ end
52
+ @tools = [agent.params[:tools], @skills, tools].flatten.compact
34
53
  end
35
54
 
36
55
  ##
@@ -38,26 +57,33 @@ module LLM
38
57
  def start
39
58
  window.open do
40
59
  loop do
41
- window.redraw
42
- text = input.readline(window)
43
- break if text.nil?
44
- next if text.empty?
45
- status.text = "thinking"
46
- write("user: #{text}\n")
47
- window.redraw
48
- write("agent: ")
49
- agent.talk(text, stream:)
50
- status.text = "idle"
51
- write("\n\n")
60
+ case input.on_char(window, window.getch)
61
+ when :exit then break
62
+ when :submit then submit
63
+ when :up, :down, :backspace, :char then window.redraw
64
+ else
65
+ window.redraw
66
+ read!
67
+ sleep 0.01
68
+ end
52
69
  end
53
70
  end
54
71
  end
55
72
 
73
+ ##
74
+ # @param [String] chars
75
+ # @param [Object] attrs
76
+ # @return [void]
77
+ def write(chars, attrs = nil)
78
+ transcript.write(chars, attrs)
79
+ window.redraw
80
+ end
81
+
56
82
  ##
57
83
  # @param [String] chars
58
84
  # @return [void]
59
- def write(chars)
60
- transcript.write(chars)
85
+ def markdown(chars)
86
+ transcript.markdown(chars)
61
87
  window.redraw
62
88
  end
63
89
 
@@ -71,8 +97,63 @@ module LLM
71
97
 
72
98
  private
73
99
 
100
+ ##
101
+ # This method is called when the user submits their input.
102
+ # It spawns a second thread that maintains a line of
103
+ # communication with a model and the main thread - where
104
+ # the UI runs - remains responsive.
105
+ # @api private
106
+ def submit
107
+ return if thread&.alive?
108
+ text = input.take
109
+ return if text.empty?
110
+ status.text = "thinking"
111
+ write("user: ", Curses::A_BOLD)
112
+ markdown(text)
113
+ write("\nagent: ", Curses::A_BOLD)
114
+ @thread = Thread.new do
115
+ @queue << [:start]
116
+ agent.talk(text, tools:, stream:)
117
+ @queue << [:done]
118
+ rescue => e
119
+ @queue << [:error, e]
120
+ end
121
+ end
122
+
123
+ ##
124
+ # This method reads from the queue that is written to
125
+ # by a second thread. The queue is managed or written
126
+ # to by a subclass of {LLM::Stream LLM::Stream}.
127
+ # @api private
128
+ def read!
129
+ loop do
130
+ type, value = @queue.pop(true)
131
+ case type
132
+ when :start
133
+ transcript.start
134
+ stream.empty!
135
+ when :stream
136
+ transcript.markdown(value, method: :replace)
137
+ when :status
138
+ self.status = value
139
+ when :done
140
+ status.text = "idle"
141
+ @thread = nil
142
+ transcript.finish
143
+ write("\n\n")
144
+ when :error
145
+ # Do this better
146
+ status.text = "error"
147
+ transcript.finish
148
+ write("\nerror: #{value.message}\n", Curses::A_BOLD)
149
+ @thread = nil
150
+ end
151
+ end
152
+ rescue ThreadError
153
+ end
154
+
74
155
  attr_reader :agent, :provider, :stream,
75
156
  :status, :transcript, :input,
76
- :window
157
+ :window, :tools, :thread
77
158
  end
78
159
  end
@@ -38,6 +38,17 @@ module LLM::Sequel
38
38
  end
39
39
 
40
40
  module InstanceMethods
41
+ ##
42
+ # @note
43
+ # This method does not persist to the database,
44
+ # but it can inspect and alter runtime state in
45
+ # a way that is temporary.
46
+ # @param (see LLM::Agent#repl)
47
+ # @return (see LLM::Agent#repl)
48
+ def repl(**params)
49
+ ctx.repl(**params)
50
+ end
51
+
41
52
  private
42
53
 
43
54
  def ctx
data/lib/llm/skill.rb CHANGED
@@ -155,7 +155,7 @@ module LLM
155
155
  def agent(ctx)
156
156
  instructions, tools, tracer, inherit_tools = self.instructions, self.tools, ctx.llm.tracer, inherit_tools?
157
157
  params = ctx.params.merge(mode: ctx.mode).reject { [:tools, :schema].include?(_1) }
158
- concurrency = params[:stream].extra[:concurrency] if LLM::Stream === params[:stream]
158
+ concurrency = params[:stream].extra[:concurrency]
159
159
  params[:concurrency] = concurrency if concurrency
160
160
  agent = Class.new(LLM::Agent) do
161
161
  instructions(instructions)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Stream
4
+ ##
5
+ # An {LLM::Stream::Disabled LLM::Stream::Disabled} represents a stream
6
+ # that is explicitly turned off.
7
+ #
8
+ # {LLM::Stream.try} returns an instance of this class when given `false`
9
+ # or `nil`. Providers check {#enabled?} to decide whether to request a
10
+ # streaming response from the API. When disabled, the stream is replaced
11
+ # with `nil` before the transport layer executes the request.
12
+ #
13
+ # All callback methods inherited from {LLM::Stream LLM::Stream} are no-ops,
14
+ # so a disabled stream can be used safely wherever a stream object is
15
+ # expected.
16
+ class Disabled < self
17
+ ##
18
+ # @return [false]
19
+ def enabled?
20
+ false
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Stream
4
+ ##
5
+ # An {LLM::Stream::IO LLM::Stream::IO} wraps an object that responds to
6
+ # `#<<` and forwards streamed content to it.
7
+ #
8
+ # This enables any object that implements `#<<` - such as an IO, StringIO,
9
+ # or a custom logger - to be used as a stream target. {LLM::Stream.try}
10
+ # creates instances of this class automatically when given an IO-like
11
+ # object.
12
+ #
13
+ # @example Using an IO object as a stream
14
+ # File.open("output.txt", "w") do |file|
15
+ # ctx.ask("Tell me a story", stream: file)
16
+ # end
17
+ class IO < self
18
+ ##
19
+ # @param [#<<] io
20
+ # Any object that implements `#<<`.
21
+ def initialize(io)
22
+ @io = io
23
+ end
24
+
25
+ ##
26
+ # Writes a chunk of content to the wrapped IO object.
27
+ # @param [String] content
28
+ # @return [void]
29
+ def on_content(content)
30
+ @io << content
31
+ end
32
+
33
+ ##
34
+ # Forwards streamed content to the wrapped IO object.
35
+ # Redefined here because +alias_method+ in the parent class
36
+ # captures the base implementation, not the overridden one.
37
+ # @param [String] content
38
+ # @return [void]
39
+ def <<(content)
40
+ on_content(content)
41
+ end
42
+ end
43
+ end
data/lib/llm/stream.rb CHANGED
@@ -21,6 +21,34 @@ module LLM
21
21
  # also emit lifecycle callbacks like {#on_transform} or {#on_compaction}.
22
22
  class Stream
23
23
  require_relative "stream/queue"
24
+ require_relative "stream/io"
25
+ require_relative "stream/disabled"
26
+
27
+ ##
28
+ # This method will try to convert its argument into
29
+ # an instance of {LLM::Stream LLM::Stream} or a
30
+ # subclass of it.
31
+ #
32
+ # Acceptable inputs include: {LLM::Stream LLM::Stream}
33
+ # objects, IO objects who implement `#<<`, true, false,
34
+ # and nil. Anything else raises a TypeError.
35
+ #
36
+ # @raise [TypeError]
37
+ # @param [LLM::Stream, #<<, Boolean, NilClass] obj
38
+ # @return [LLM::Stream]
39
+ def self.try(obj, extra: {})
40
+ if LLM::Stream === obj
41
+ obj.tap { _1.extra.merge!(extra) }
42
+ elsif obj.respond_to?(:<<)
43
+ LLM::Stream::IO.new(obj).tap { _1.extra.merge!(extra) }
44
+ elsif obj == true
45
+ LLM::Stream.new.tap { _1.extra.merge!(extra) }
46
+ elsif obj.nil? || obj == false
47
+ LLM::Stream::Disabled.new.tap { _1.extra.merge!(extra) }
48
+ else
49
+ raise TypeError, "invalid stream object"
50
+ end
51
+ end
24
52
 
25
53
  ##
26
54
  # Returns extra context associated with the current streamed request.
@@ -52,6 +80,12 @@ module LLM
52
80
  queue.wait
53
81
  end
54
82
 
83
+ ##
84
+ # @return [Boolean]
85
+ def enabled?
86
+ true
87
+ end
88
+
55
89
  # @group Public callbacks
56
90
 
57
91
  ##
data/lib/llm/tools/git.rb CHANGED
@@ -16,9 +16,8 @@ class LLM::Tool
16
16
  required %i[action]
17
17
 
18
18
  ##
19
- # @param [String] path
20
- # @param [Integer] start
21
- # @param [Integer] stop
19
+ # @param [String] action
20
+ # @param [Array<String>, nil] arguments
22
21
  # @return [Hash]
23
22
  def call(action:, arguments: nil)
24
23
  command = spawn(action:, arguments:)
data/lib/llm/tools/pwd.rb CHANGED
@@ -11,7 +11,6 @@ class LLM::Tool
11
11
  description "returns the current working directory"
12
12
 
13
13
  ##
14
- # @param [String] path
15
14
  # @return [Hash]
16
15
  def call
17
16
  {ok: true, cwd: Dir.getwd}
data/lib/llm/tools/rg.rb CHANGED
@@ -16,7 +16,8 @@ class LLM::Tool
16
16
  required %i[patterns]
17
17
 
18
18
  ##
19
- # @param [String] pattern
19
+ # @param [Array<String>] patterns
20
+ # @param [String] path
20
21
  # @return [Hash]
21
22
  def call(patterns:, path: Dir.getwd)
22
23
  validate!(patterns:, path:)
@@ -14,6 +14,12 @@ class LLM::Tool
14
14
  parameter :expected_count, Integer, "How many matches should be replaced"
15
15
  required %i[path before after]
16
16
 
17
+ ##
18
+ # @param [String] path
19
+ # @param [String] before
20
+ # @param [String] after
21
+ # @param [Integer] expected_count
22
+ # @return [Hash]
17
23
  def call(path:, before:, after:, expected_count: 1)
18
24
  content = File.read(path)
19
25
  count = content.scan(before).length
@@ -29,6 +29,7 @@ class LLM::Transport
29
29
  # When there is a network error at the operating system level
30
30
  # @return [LLM::Transport::Response]
31
31
  def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, inputs: nil, &b)
32
+ stream = nil if !stream&.enabled?
32
33
  stream &&= LLM::Object.from(streamer: stream, parser: stream_parser, decoder: stream_decoder)
33
34
  owner = transport.request_owner
34
35
  tracer = self.tracer
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "12.1.0"
4
+ VERSION = "12.3.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -180,6 +180,15 @@ module LLM
180
180
  LLM::XAI.new(**)
181
181
  end
182
182
 
183
+ ##
184
+ # @param key (see LLM::Mistral#initialize)
185
+ # @param host (see LLM::Mistral#initialize)
186
+ # @return (see LLM::Mistral#initialize)
187
+ def mistral(**)
188
+ lock(:require) { require_relative "llm/providers/mistral" unless defined?(LLM::Mistral) }
189
+ LLM::Mistral.new(**)
190
+ end
191
+
183
192
  ##
184
193
  # @param key (see LLM::ZAI#initialize)
185
194
  # @param host (see LLM::ZAI#initialize)
@@ -243,6 +252,16 @@ module LLM
243
252
  LLM::Function.new(key, &b)
244
253
  end
245
254
 
255
+ ##
256
+ # @param [LLM::Provider] llm
257
+ # A provider
258
+ # @param [Hash] params
259
+ # Forwarded to LLM::Tracer::Logger
260
+ # @return [LLM::Tracer::Logger]
261
+ def logger(llm, **params)
262
+ LLM::Tracer::Logger.new(llm, params)
263
+ end
264
+
246
265
  ##
247
266
  # Provides a thread-safe lock
248
267
  # @param [Symbol] name The name of the lock
data/llm.gemspec CHANGED
@@ -5,13 +5,17 @@ require_relative "lib/llm/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "llm.rb"
7
7
  spec.version = LLM::VERSION
8
- spec.authors = ["bsdrobert", "Antar Azri", "Rodrigo Serrano"]
8
+ spec.authors = ["robert", "Antar Azri", "Rodrigo Serrano"]
9
9
  spec.email = ["robert@r.uby.dev"]
10
10
 
11
11
  spec.summary = "Ruby's capable AI runtime"
12
- spec.description = "llm.rb is not a library, framework or toolkit but " \
13
- "an advanced runtime for building highly capable AI " \
14
- "applications on CRuby."
12
+ spec.description = <<~DESCRIPTION
13
+ llm.rb is an advanced runtime for building capable AI applications
14
+ on CRuby. By default it has zero runtime dependencies although certain
15
+ functionality &ndash; such as ActiveRecord support &ndash; require
16
+ optional dependencies that are opt-in.
17
+ DESCRIPTION
18
+
15
19
  spec.license = "BUSL-1.1"
16
20
  spec.required_ruby_version = ">= 3.3.0"
17
21
 
@@ -25,44 +25,126 @@ features that didn't make it into the homepage documentation.
25
25
 
26
26
  ## Table of contents
27
27
 
28
- - [Agents](#agents)
29
- - [As a subclass](#as-a-subclass)
30
- - [As an object](#as-an-object)
31
- - [Skills](#skills)
32
- - [SKILL.md](#skillmd)
33
- - [Run it](#run-it)
34
- - [MCP](#mcp)
35
- - [stdio](#stdio)
36
- - [http](#http)
37
- - [A2A](#a2a)
38
- - [rest](#rest)
39
- - [jsonrpc](#jsonrpc)
40
- - [Transports](#transports)
41
- - [net/http](#nethttp)
42
- - [net/http/persistent](#nethttppersistent)
43
- - [curb](#curb)
44
- - [Stream](#stream)
45
- - [IO-like object](#io-like-object)
46
- - [LLM::Stream](#llmstream)
47
- - [ORM](#orm)
48
- - [ActiveRecord](#activerecord)
49
- - [Sequel](#sequel)
50
- - [Schema](#schema)
51
- - [Estimation](#estimation)
52
- - [Cancellation](#cancellation)
53
- - [Cancel a request](#cancel-a-request)
54
- - [Tracer](#tracer)
55
- - [Provider-wide tracer](#provider-wide-tracer)
56
- - [Agent-local tracer](#agent-local-tracer)
57
- - [REPL](#repl)
58
- - [LLM::Agent](#llmagent)
59
- - [Images](#images)
60
- - [Generation](#generation)
61
- - [Edits](#edits)
62
- - [Audio](#audio)
63
- - [text-to-speech](#text-to-speech)
64
- - [speech-to-text](#speech-to-text)
65
- - [translation](#translation)
28
+ **Overview**
29
+
30
+ - [Welcome](#welcome)
31
+
32
+ **Core**
33
+
34
+ <details>
35
+ <summary>Agents</summary>
36
+
37
+ - [As a subclass](#as-a-subclass)
38
+ - [As an object](#as-an-object)
39
+ </details>
40
+
41
+ <details>
42
+ <summary>Tools</summary>
43
+
44
+ - [LLM::Tool](#llmtool)
45
+ - [Errors](#errors)
46
+ </details>
47
+
48
+ <details>
49
+ <summary>Skills</summary>
50
+
51
+ - [SKILL.md](#skillmd)
52
+ - [Run it](#run-it)
53
+ </details>
54
+
55
+ <details>
56
+ <summary>Schema</summary>
57
+
58
+ - [Estimation](#estimation)
59
+ </details>
60
+
61
+ **Runtime**
62
+
63
+ <details>
64
+ <summary>Stream</summary>
65
+
66
+ - [IO-like object](#io-like-object)
67
+ - [LLM::Stream](#llmstream)
68
+ </details>
69
+
70
+ <details>
71
+ <summary>Cancellation</summary>
72
+
73
+ - [Cancel a request](#cancel-a-request)
74
+ </details>
75
+
76
+ <details>
77
+ <summary>Transports</summary>
78
+
79
+ - [net/http](#nethttp)
80
+ - [net/http/persistent](#nethttppersistent)
81
+ - [curb](#curb)
82
+ </details>
83
+
84
+ <details>
85
+ <summary>Tracer</summary>
86
+
87
+ - [Provider-wide tracer](#provider-wide-tracer)
88
+ - [Agent-local tracer](#agent-local-tracer)
89
+ </details>
90
+
91
+ <details>
92
+ <summary>REPL</summary>
93
+
94
+ - [LLM::Agent](#llmagent)
95
+ - [Tools](#tools)
96
+ - [Skills](#skills-1)
97
+ - [Tracer](#tracer-1)
98
+ </details>
99
+
100
+ **Persistence**
101
+
102
+ <details>
103
+ <summary>Serialization</summary>
104
+
105
+ - [Save to disk](#save-to-disk)
106
+ </details>
107
+
108
+ <details>
109
+ <summary>ORM</summary>
110
+
111
+ - [ActiveRecord](#activerecord)
112
+ - [Sequel](#sequel)
113
+ </details>
114
+
115
+ **Media**
116
+
117
+ <details>
118
+ <summary>Images</summary>
119
+
120
+ - [Generation](#generation)
121
+ - [Edits](#edits)
122
+ - [DeepSeek](#deepseek)
123
+ </details>
124
+
125
+ <details>
126
+ <summary>Audio</summary>
127
+
128
+ - [text-to-speech](#text-to-speech)
129
+ - [speech-to-text](#speech-to-text)
130
+ - [translation](#translation)
131
+ </details>
132
+
133
+ **Protocols**
134
+
135
+ <details>
136
+ <summary>MCP</summary>
137
+
138
+ - [stdio](#stdio)
139
+ - [http](#http)
140
+ </details>
141
+
142
+ <details>
143
+ <summary>A2A</summary>
144
+
145
+ - [rest](#rest)
146
+ - [jsonrpc](#jsonrpc)
147
+ </details>
66
148
 
67
149
  ## Agents
68
150
 
@@ -808,21 +890,22 @@ agent = LLM::Agent.new(llm, tracer: LLM::Tracer::Logger.new(llm, path: "deepseek
808
890
 
809
891
  During the development and operation of agents it can often
810
892
  be helpful to drop into a read-eval-print loop. This gives
811
- you a simple way to confirm the work was successful, inspect
812
- anything that went wrong (for example, tool call errors), and
813
- keep talking to the same agent so you can learn about its
814
- decision-making process and make improvements for future runs.
815
- It can also be used to ask the agent to correct any errors
816
- that might have made.
893
+ you a way to confirm the work was successful, inspect
894
+ anything that went wrong, and keep talking to the same
895
+ agent while its state is still intact.
817
896
 
818
- ##### LLM::Agent
897
+ #### LLM::Agent
819
898
 
820
899
  The [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
821
- method requires the [curses](https://github.com/ruby/curses)
822
- gem to be installed and available for require. It is an
823
- optional dependency that only becomes required when you
824
- call the [LLM::Agent#repl](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#repl-instance_method)
825
- method.
900
+ method allows an agent to spawn a read-eval-print loop
901
+ that can be useful while developing or operating agents.
902
+ It can be used to debug tool calls, confirm an
903
+ agent has done what was expected, or improve an agent by
904
+ asking questions about what it has done up to that point.
905
+
906
+ This feature requires that the [curses](https://github.com/ruby/curses)
907
+ and [kramdown](https://github.com/gettalong/kramdown) libraries are
908
+ installed and available to require.
826
909
 
827
910
  ```ruby
828
911
  require "llm"
@@ -832,6 +915,43 @@ agent = LLM::Agent.new(llm)
832
915
  agent.repl
833
916
  ```
834
917
 
918
+ #### Tools
919
+
920
+ The read-eval-print loop accepts a `tools` option that lets
921
+ you attach additional tools for the duration of the session.
922
+
923
+ ```ruby
924
+ llm = LLM.deepseek(key: ENV["KEY"])
925
+ agent = LLM::Agent.new(llm)
926
+ agent.repl(tools: [Debugger])
927
+ ```
928
+
929
+ #### Skills
930
+
931
+ The read-eval-print loop also accepts a `skills` option.
932
+ This can be useful when you want to load extra skills
933
+ without attaching them to an agent permanently.
934
+
935
+ ```ruby
936
+ llm = LLM.deepseek(key: ENV["KEY"])
937
+ agent = LLM::Agent.new(llm)
938
+ agent.repl(skills: [__dir__])
939
+ ```
940
+
941
+ #### Tracer
942
+
943
+ By default the tracer is disabled for the duration of the
944
+ session. This can be configured through the
945
+ `tracer` option. Setting it to `true` will configure
946
+ the REPL to use the tracer associated with an instance
947
+ of [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
948
+
949
+ ```ruby
950
+ llm = LLM.deepseek(key: ENV["KEY"])
951
+ agent = LLM::Agent.new(llm, tracer: LLM.logger(llm, path: "agent.log"))
952
+ agent.repl(tracer: true, tools: [Debugger])
953
+ ```
954
+
835
955
  [Back to top](#table-of-contents)
836
956
 
837
957
  ## Images